把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【题解】P3354 [IOI2005]Riv 河流 - 树形 $dp$ - 孩子兄弟表示法

P3354 [IOI2005]Riv 河流

声明:本博客所有题解都参照了网络资料或其他博客,仅为博主想加深理解而写,如有疑问欢迎与博主讨论✧。٩(ˊᗜˋ)و✧*。

题目描述

几乎整个 \(Byteland\) 王国都被森林和河流所覆盖。小点的河汇聚到一起,形成了稍大点的河。就这样,所有的河水都汇聚并流进了一条大河,最后这条大河流进了大海。这条大河的入海口处有一个村庄——名叫 \(Bytetown\)

\(Byteland\) 国,有 \(n\) 个伐木的村庄,这些村庄都座落在河边。目前在 \(Bytetown\),有一个巨大的伐木场,它处理着全国砍下的所有木料。木料被砍下后,顺着河流而被运到 \(Bytetown\) 的伐木场。\(Byteland\) 的国王决定,为了减少运输木料的费用,再额外地建造 \(k\) 个伐木场。这 \(k\) 个伐木场将被建在其他村庄里。这些伐木场建造后,木料就不用都被送到 \(Bytetown\) 了,它们可以在运输过程中第一个碰到的新伐木场被处理。显然,如果伐木场座落的那个村子就不用再付运送木料的费用了。它们可以直接被本村的伐木场处理。

注:所有的河流都不会分叉,形成一棵树,根结点是 \(Bytetown\)

国王的大臣计算出了每个村子每年要产多少木料,你的任务是决定在哪些村子建设伐木场能获得最小的运费。其中运费的计算方法为:每一吨木料每千米 \(1\) 分钱。

\(\\\)


\(\\\)

\(Solution\)

先从正常的套路树形 \(dp\) 方程想起,设 \(dp[i][j]\) 表示在 \(i\)\(i\) 子树中建立 \(j\) 个伐木场的代价

但是这样是无法转移的——对于每个 \(dp[i][j]\) 无法得知那些未被解决的木头的贡献,如果设 \(g[i][j]\) 来记录未被解决的木头书的话,那这个是有后效性的,也就是说,每个 \(i\) 的状态对于不同的 \(i\) 的祖先的影响是不同的,无法正常 \(dp\)

为了把后效性消除掉,我们必须得知一个祖先的状态来处理掉这些未被解决的木头

顺理成章的 (但是如果没有写过这种带祖先状态的树形 \(dp\) 应该还是不容易想到的,比如我233)我们加一维状态变为 \(dp[i][j][l]\) 表示在 \(i\)\(i\) 子树中建立 \(j\) 个伐木场,且从 \(i\) 往祖先方向走 \(l\) 步会碰到第一个伐木场

由于我们可以从方程中得到最近的一个祖先是谁,所以那些未被解决的木头的费用是固定的,可计算的,即可 \(dp\)

搬运代码来自:Link

void dfs(int i){
    stack[++size]=i;
    for(int tmp=head[i];tmp;tmp=nxt[tmp]){
        int v=point[tmp];
        deep[v]=deep[i]+weight[tmp];
        dfs(v);
        for(int j=1;j<=size;j++)
            for(int k=K;k>=0;k--){
                f[i][stack[j]][k]+=f[v][stack[j]][0];
                g[i][stack[j]][k]+=f[v][i][0];
                for(int x=0;x<=k;x++){
                    f[i][stack[j]][k]=min(f[i][stack[j]][k],f[i][stack[j]][k-x]+f[v][stack[j]][x]);
                    g[i][stack[j]][k]=min(g[i][stack[j]][k],g[i][stack[j]][k-x]+f[v][i][x]);
                }
            }
    }
    //这里是将f和g合并了,因为之后就不在乎i有没有建伐木场,只关心i和i的子树建了多少。
    for(int j=1;j<=size;j++)
        for(int k=0;k<=K;k++){
            if(k>=1)
                f[i][stack[j]][k]=min(f[i][stack[j]][k]+sum[i]*(deep[i]-deep[stack[j]]),g[i][stack[j]][k-1]);
    //这里是g[i][stack[j]][k-1]的原因是:因为我们之前算g的时候,是假设i上有伐木场。而我们实际上没有把这个伐木场的数量加进去。所以合并前g[i][j][k]实际上代表的是g[i][j][k+1]
            else 
                f[i][stack[j]][k]+=sum[i]*(deep[i]-deep[stack[j]]);
        }
        
    size--;
}

完结撒花✿✿ヽ(°▽°)ノ✿

才不会233

看标题显然发现还有另一个超可爱超有趣超神奇的!内容

你每次写树形 \(dp\) 的儿子合并都要为各种边界烦恼吗?

你厌倦了链式前向星存边吗?

来试试孩子兄弟表示法吧!(牛津高中英语M4U1套句 qwq

参考博客:Link

对于每个节点,保存四个值:\(bro\) - 指向这个点一个兄弟,\(son\) - 指向这个点的儿子,\(fa\) - 指向这个点的父亲,\(edge\) - 保存这个点与父亲的边权值

例如在这个图中,\(4, 5, 6\) 分别为兄弟,\(3\)\(7\) 的父亲,\(8\)\(7\) 的儿子

那么用孩子兄弟表示法就会转化为下面这张图

发现这是一个二叉图,为了方便理解,此张图中每个点指向的左儿子为它的儿子,右儿子为它的兄弟

关于如何构建这颗树:

for(int i = 1; i <= n; ++ i) fa[i] = read(), bro[i] = son[fa[i]], son[fa[i]] = i;

我们按照这样的树进行 \(dfs\) 时,它的一些兄弟就暂时的成为了它的子树的一部分,而在这道题中,由于兄弟间是互相不影响的,所以一个节点可以存储它兄弟的信息,同时可以更新它另外一些兄弟的信息,因为这些兄弟们总是在它们的父亲的子树中的,所以答案总会被更新出来

那么这样写有什么好处呢?!

上代码啦啦啦(*)

void dfs(int x, int dep)
{
   int bro = b[x], son = s[x];
   F(i, 1, dep) dis[x][i] = dis[f[x]][i - 1] + e[x];
   if(bro) dfs(bro, dep);
   if(son) dfs(son, dep + 1);
   F(i, 0, k)
      F(j, 0, k - i)
         F(l, 1, dep)
         {
            dp[x][i + j][l] = min(dp[x][i + j][l], dp[bro][i][l] + dp[son][j][l + 1] + w[x] * dis[x][l]);
            dp[x][i + j + 1][l] = min(dp[x][i + j + 1][l], dp[bro][i][l] + dp[son][j][1]);
         }
}

对比上面的代码可以直观地发现,这样子省去了各种合并,由于每个结点只有两个儿子,就用两个循环更新即可!

最后的答案就是 \(dp[son[0]][k][1]\) ~

真——完结撒花✿✿ヽ(°▽°)ノ✿

\(\\\)


\(\\\)

\(Code\)

#include<bits/stdc++.h>
#define F(i, x, y) for(int i = x; i <= y; ++ i)
using namespace std;
int read();
const int N = 105;
int n, k, dp[N][55][N];
int b[N], s[N], f[N], e[N], w[N], dis[N][N];
void dfs(int x, int dep)
{
   int bro = b[x], son = s[x];
   F(i, 1, dep) dis[x][i] = dis[f[x]][i - 1] + e[x];
   if(bro) dfs(bro, dep);
   if(son) dfs(son, dep + 1);
   F(i, 0, k)
   F(j, 0, k - i)
      F(l, 1, dep)
      {
         dp[x][i + j][l] = min(dp[x][i + j][l], dp[bro][i][l] + dp[son][j][l + 1] + w[x] * dis[x][l]);
         dp[x][i + j + 1][l] = min(dp[x][i + j + 1][l], dp[bro][i][l] + dp[son][j][1]);
      }
}
int main()
{
   n = read(), k = read();
   F(i, 1, n) 
   {
      w[i] = read(), f[i] = read(), e[i] = read();
      b[i] = s[f[i]], s[f[i]] = i;
   }
   memset(dp, 0x3f, sizeof(dp));
   F(i, 0, k) F(j, 0, n) dp[0][i][j] = 0;
   dfs(s[0], 1), printf("%d\n", dp[s[0]][k][1]);
   return 0;
}
int read()
{
   int x = 0, f = 1;
   char c = getchar();
   while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
   while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
   return x * f;
}	
posted @ 2020-06-09 22:20  Bn_ff  阅读(193)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end