★【树型动态规划】【NOI2008】奥运物流

来源:互联网 发布:select in sql值太多 编辑:程序博客网 时间:2024/05/22 05:08
【问题描述】  2008 北京奥运会即将开幕,举国上下都在为这一盛事做好准备。为了高效率、成功地举办奥运会,对物流系统进行规划是必不可少的。  物流系统由若干物流基站组成,以 1...N 进行编号。每个物流基站 i 都有且仅有一个后继基站 Si,而可以有多个前驱基站。基站 i 中需要继续运输的物资都将被运往后继基站 Si,显然一个物流基站的后继基站不能是其本身。编号为 1 的从任何物流基站都可将物资运往控制基站。  物流基站称为控制基站,注意控制基站也有后继基站,以便在需要时进行物资的流通。在物流系统中,高可靠性与低成本是主要设计目。对于基站 i,我们定义其“可靠性” R (i ) 如下:  设物流基站 i 有 w 个前驱基站 P1, P2, Pw,即这些基站以 i 为后继基站,则基站 i 的可靠性 R(i)满足下式: 


其中 Ci 和 k 都是常实数且恒为正,且有 k 小于 1。  整个系统的可靠性与控制基站的可靠性正相关,我们的目标是通过修改物流系统,即更改某些基站的后继基站,使得控制基站的可靠性 R(1)尽量大。但由于经费限制,最多只能修改 m 个基站的后继基站,并且,控制基站的后继基站不可被修改。因而我们所面临的问题就是,如何修改不超过 m 个基站的后继,使得控制基站的可靠性 R(1)最大化。  【输入格式】  输入文件 trans.in 第一行包含两个整数与一个实数,N, m, k。其中 N 表示基站数目,m 表示最多可修改的后继基站数目,k 分别为可靠性定义中的常数。  第二行包含 N 个整数,分别是 S1, S2...SN,即每一个基站的后继基站编号。  第三行包含 N 个正实数,分别是 C1, C2...CN,为可靠性定义中的常数。  【输出格式】  输出文件 trans.out 仅包含一个实数,为可得到的最大 R(1)。精确到小数点两位。  【输入样例】  4 1 0.5  2 3 1 3  10.0 10.0 10.0 10.0  【输出样例】  30.00  【样例说明】  原有物流系统如左图所示,4 个物流基站的可靠性依次为 22.8571,21.4286, 25.7143,10。  

最优方案为将 2 号基站的后继基站改为 1 号,如右图所示。 此时 4 个基站的可靠性依次为 30,25,15,10。  【数据规模和约定】  本题的数据,具有如下分布:

对于所有的数据,满足 m ≤ N ≤ 60,Ci ≤ 106,0.3 ≤ k < 1,请使用双精度实数,无需考虑由此带来的误差。  
考试的时候,直接没管这道题,于是0分,后来听说贪心能得70分,很无语……

整个图就是一棵树,树根上是一个环,若设环的长度为len,那么整个答案为:

那么每个点离根越近答案越优,于是m次修改一定是将其中m个点的后继基站改为1。

用树型动态规划解决此问题:
首先枚举根部的环的长度,对每一个长度的环都进行一次动态规划,取出所有的最大值(注意修改环的长度可能会造成一次修改机会的消耗)作为最终答案。

用f[u][c][d]表示以u为根的子树中,修改c次,且u到根的距离为d的最大值(所有子树均不考虑1的后继边)。
对于非1的u,转移方程如下:

可以令g[u][c][d] = max{f[u][c][d + 1], f[u][c][1]}以简化方程,
并且可以发现右边的两项其实就是对c的一个最大分配,所以可以用多重背包问题解决。
但要注意u = 1时,方程右边只有第一项,并且其中的g[u][c][d]为f[u][c][1]。
代码:

/**************************\ * @prob: NOI2008 trans   * * @auth: Wang Junji      * * @stat: Accepted.       * * @date: May. 21st, 2012 * * @memo: 树型动态规划      *\**************************/#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <string>const int maxN = 80;double f[maxN][maxN][maxN], g[maxN][maxN][maxN];double C[maxN], F[maxN], K[maxN], ans;int pre[maxN], n, m;void Dp(int u, int d){    for (int v = 2; v < n + 1; ++v) if (pre[v] == u) Dp(v, d + 1);    for (int _d = std::min(2, d); _d < d + 1; ++_d)        //由于当前的d值可能被修改过(有可能它的每一个祖先        //被指向了1),所以从2开始枚举d。        //(特别地,d = 1时不可能被修改过。)    {        for (int j = 0; j < m + 1; ++j) F[j] = 0;        for (int v = 2; v < n + 1; ++v) if (pre[v] == u)        for (int j = m; j > -1; --j)        for (int k = j; k > -1; --k)            F[j] = std::max(F[j], F[k] + g[v][j - k][_d]);        //对其各个子树进行多重背包。        for (int j = 0; j < m + 1; ++j)            f[u][j][_d] = F[j] + C[u] * K[_d];    }    if (d > 1) //d > 1时,将其后继结点修改为1才有意义。    {        for (int j = 0; j < m + 1; ++j) F[j] = 0;        for (int v = 2; v < n + 1; ++v) if (pre[v] == u)        for (int j = m; j > -1; --j)        for (int k = j; k > -1; --k)            F[j] = std::max(F[j], F[k] + g[v][j - k][1]);        for (int j = 1; j < m + 1; ++j)            f[u][j][1] = F[j - 1] + C[u] * K[1];    }    for (int j = 0; j < m + 1; ++j)    for (int _d = 0; _d < d; ++_d)        g[u][j][_d] = std::max(f[u][j][_d + 1], f[u][j][1]);    return;}int main(){    freopen("trans.in", "r", stdin);    freopen("trans.out", "w", stdout);    scanf("%d%d%lf", &n, &m, K + 1);    for (int i = 2; i < n + 1; ++i) K[i] = K[i - 1] * K[1];    for (int i = 1; i < n + 1; ++i) scanf("%d", pre + i);    for (int i = 1; i < n + 1; ++i) scanf("%lf", C + i);    for (int ths = pre[1], len = 2; ths - 1; ths = pre[ths], ++len)    //尝试分别将每一个处在环上的结点的父结点修改成1,    //然后分别做一次动态规划。    {        for (int i = 1; i < n + 1; ++i)        for (int j = 0; j < m + 1; ++j)        for (int k = 0; k < n + 1; ++k)            f[i][j][k] = g[i][j][k] = 0;        int tmp = pre[ths]; double Now = 0;        pre[ths] = 1;        for (int v = 2; v < n + 1; ++v) if (pre[v] == 1) Dp(v, 1);        for (int j = 0; j < m + 1; ++j) F[j] = 0;        for (int v = 2; v < n + 1; ++v) if (pre[v] == 1)        for (int j = m; j > -1; --j)        for (int k = j; k > -1; --k)            F[j] = std::max(F[j], F[k] + f[v][j - k][1]);        //由于1的直接子结点不可能被修改过,        //所以上面只能为f[v][j - k][1]不能为g[v][j - k][1]。        for (int j = 0; j < m; ++j) Now = std::max(Now, F[j]);        if (tmp == 1) Now = std::max(Now, F[m]);        //若ths的父结点本身就是1,说明剩余的子树可以用m次修改机会。        ans = std::max(ans, (Now + C[1]) / (1 - K[len]));        pre[ths] = tmp; //将ths的父结点还原。    }    printf("%.2lf\n", ans);    return 0;}