树形dp(IOI 2005河流代码理解)

2 篇文章 0 订阅

题目描述

几乎整个Byteland王国都被森林和河流所覆盖。小点的河汇聚到一起,形成了稍大点的河。就这样,所有的河水都汇聚并流进了一条大河,最后这条大河流进了大海。这条大河的入海口处有一个村庄——名叫Bytetown
在Byteland国,有n个伐木的村庄,这些村庄都座落在河边。目前在Bytetown,有一个巨大的伐木场,它处理着全国砍下的所有木料。木料被砍下后,顺着河流而被运到Bytetown的伐木场。Byteland的国王决定,为了减少运输木料的费用,再额外地建造k个伐木场。这k个伐木场将被建在其他村庄里。这些伐木场建造后,木料就不用都被送到Bytetown了,它们可以在 运输过程中第一个碰到的新伐木场被处理。显然,如果伐木场座落的那个村子就不用再付运送木料的费用了。它们可以直接被本村的伐木场处理。
注意:所有的河流都不会分叉,也就是说,每一个村子,顺流而下都只有一条路——到bytetown。
国王的大臣计算出了每个村子每年要产多少木料,你的任务是决定在哪些村子建设伐木场能获得最小的运费。其中运费的计算方法为:每一块木料每千米1分钱。
编一个程序:
1.从文件读入村子的个数,另外要建设的伐木场的数目,每年每个村子产的木料的块数以及河流的描述。
2.计算最小的运费并输出。

输入

第1行:包括两个数 n(2<=n<=100),k(1<=k<=50,且 k<=n)。n为村庄数,k为要建的伐木场的数目。除了bytetown外,每个村子依次被命名为1,2,3……n,bytetown被命名为0。

接下来n行,每行3个整数 wi——每年i村子产的木料的块数 (0<=wi<=10000) vi——离i村子下游最近的村子(即i村子的父结点)(0<=vi<=n) di——vi到i的距离(千米)。(1<=di<=10000)

保证每年所有的木料流到bytetown的运费不超过2000,000,000分 50%的数据中n不超过20。


输出

输出最小花费,精确到分。

样例输入

4 2
1 0 1
1 1 10
10 2 5
1 2 3

样例输出

4

说明

下图是样例输入的说明。圈内数字为村庄编号,下方数字为存储木料块数,边权为河的长度。伐木场应建在村庄2和3。




应该比较好看出来这是一道树形dp,而dp方程式有点难想,刚开始时自己想只有两维,分别是当前村庄i,和所需建的伐木场个数k。然后怎么想也想不出来,于是就搜了一下题解。
下面是搜出来的题解代码(有改动)
#include<iostream>
#include<cmath>
#include<climits>
#include<cstdio>
#include<cstring>
#include<algorithm>

typedef unsigned long long uLL;
const unsigned long long inf=2000000005;
const int MAXN=100;
using namespace std;
struct node{int rc,lc,w,d;}tree[MAXN+5];
int cnt,n,K,a,dist[MAXN+5],H[MAXN+5],fa[MAXN+5];
uLL F[MAXN+5][MAXN+5][55];
bool vis[MAXN+5];

void dfs(int s,int deep){
    H[++cnt]=s; vis[s]=1; dist[s]=deep;
    for(int i=tree[s].lc;i;i=tree[i].rc)
        if(!vis[i]) dfs(i,deep+tree[i].d);
}
inline void Read(int &Ret){
    char ch; bool flag=0;
    for(;ch=getchar(),ch<'0'||ch>'9';)if(ch=='-') flag=1;
    for(Ret=ch-'0';ch=getchar(),'0'<=ch&&ch<='9';Ret=Ret*10+ch-'0');
    flag&&(Ret=-Ret);
}
int main()
{
    Read(n); Read(K);
    memset(fa,-1,sizeof(fa));
    for(int i=1;i<=n;i++)
    {
       Read(tree[i].w); Read(a); Read(tree[i].d);
       tree[i].rc=tree[a].lc;
       tree[a].lc=i; fa[i]=a;
    }
    dfs(0,0);
    memset(F,inf,sizeof(F));
    memset(F[0],0,sizeof(F[0]));
    for(int i=n+1;i>=2;i--)
    {
        int now=H[i];
        int lson=tree[now].lc,rson=tree[now].rc;
        for(int j=fa[now];j!=-1;j=fa[j])
            for(int k=0;k<=K;k++)
            {
                for(int l=0;l<=k;l++)//不选 i
                    if(F[lson][j][l]!=inf&&F[rson][j][k-l]!=inf)
                    {
                        uLL add=(uLL)tree[now].w*(dist[now]-dist[j]);
                        F[now][j][k]=min(F[now][j][k],F[lson][j][l]+F[rson][j][k-l]+add);
                    }
                for(int l=0;l<k;l++)//选 i
                    if(F[lson][now][l]!=inf&&F[rson][j][k-l-1]!=inf)
                        F[now][j][k]=min(F[now][j][k],F[lson][now][l]+F[rson][j][k-l-1]);
            }
    }
    printf("%llu",F[tree[0].lc][0][K]);
}



第一眼看过去的时候就觉得很诡异,因为这位大神竟然没有用到递归,而是直接套了四层循环搞了出来。
dp的状态有三维:
用 f[i][j][k] 表示目前考虑第i个点,j表示i所有的祖先中离i最近并且建了伐木场的节点,i的儿子以及i的兄弟一共建了k个伐木场。
(在i建)f[i][j][k]=min(f[i][j][k],f[lson][i][l]+f[rson][j][k-l-1])
(不在i建)f[i][j][k]=min(f[i][j][k],f[lson][j][l]+f[rson][j][k-l]+val[i]*(dis[i][j]))
这是直接copy的那位大神的叙述。应该是比较清晰的。注意这是已经转成二叉数后的方程式。
解释一下:
当在i建时,离i最近的伐木场自然是i本身,而他的兄弟节点(也就是他的rson,转了二叉),最近的依然是j,因为i已建立伐木场,所以i的儿子还有l个伐木场可建,而兄弟就还有k-l-1个伐木场可建
而不在i建时,离i和它的兄弟们最近的伐木场自然仍然是j,其余同上。而这时需要加上木材运到j的花费了(上面的那种状态不用的原因是因为还未确定是否在除i以外的其他节点建造伐木场,i本身是没有花费的)

都说编程有三难:思路难,代码难,调试难。
将思路解决了,在来看看这个不用递归的神奇代码

在处理输入的时候就是把多叉转成二叉,并没有什么特殊的处理。
而需要注意的是dfs:
这个dfs首先会将每个节点距根节点的距离处理出来,存储在dist数组中
然后给每个节点赋予一个按照先序遍历的顺序的编号,在枚举节点的时候,就是按这个编号的从大到小来枚举的。

那么为什么可以按照先序遍历来处理呢?
画个图可以发现,每一个子树本身必定大于它的所有的儿子节点,或者更准确地说,对于每一个子树i,它的编号都刚好比它的某个儿子小1,也就意味着,除叶子节点以外,每个节点都能保证它所有儿子节点都处理完后再来处理它本身,那么恰好满足dp的状态转移所需要的顺序了。(表示深深的膜拜)

然后是有一句话memset(F[0],0,sizeof(F[0]));
我之前一直都没想通,为何要把它清为0。
后来有点明白了。
注意,这里的F[0]并不是指的根节点,而是指叶子节点的下方节点,如果它们的值也是inf,那么你会发现,dp是跑不动的。

然后就没啥了,其余还有点细节问题

再次表示深深的膜拜

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值