树上的动规
树形dp,在没有写过之前还以为十分的难,其实掌握了基本的几种之后还是比较容易的。
还是回到做dp最难的一个步骤,如何定义状态?
我们对于一颗树,无非考虑的问题有几个,当前的根是谁?它的左右孩子如何处理?我们探讨一下这个几个问题。
我将用题目来一起代入,算是树形dp入门题了。
先来一道普适性最高的题目
洛谷P2015 二叉苹果树
首先我们定义F[ x ][ i ]表示当前为x 作为根节点,在这个根节点下面留住 i 条树枝 的所能留住的最多苹果的数量,则答案就是F[ 1 ][ m ].
那么我们怎么实现呢?
对于当前点,我们肯定是要知道子节点的最佳状态是多少才能做出抉择,那么我们只能递归这颗树,自下往上进行操作。
对于叶子节点,F 值就为0;那么对于一个不为叶子节点,有孩子的点怎么做出抉择呢?
我们考虑要在递归的时候算出每一个节点下面有多少个孩子,然后进行枚举考虑要留住多少个孩子,当然如果孩子数目小于要留住的m个枝条数目,那么只用枚举到孩子的数目;同理,留住的m个枝条数目大于孩子的数目。
于是我们可以写出 F[ x ][ i ]=max(F [ x ] [ i ],F[ x ][ i-j-1 ]+F[ u ][ j ]+value[k]); (u 是当前根节点x的子节点,F[ u ][ j ]表示子节点u留 j 条边的最大值,那么F[ x ][ i ]表示当前根节点留 i 条边的最大值,i>j)
#include <bits/stdc++.h> #define maxn 1005 using namespace std; int tot,next[maxn],first[105],zhi[maxn],value[maxn],x,y,v,n,m,i,f[105][maxn],size[105]; void build(int x,int y,int v) { tot++; next[tot]=first[x]; first[x]=tot; zhi[tot]=y; value[tot]=v; } void dfs(int x,int father) { int k,i,j,u; k=first[x]; while (k!=-1) { u=zhi[k]; if (u==father) { k=next[k]; continue; } dfs(u,x); size[x]+=size[u]+1; for (i=min(size[x],m);i>=0;i--) for (j=min(size[u],i-1);j>=0;j--) f[x][i]=max(f[x][i],f[x][i-j-1]+f[u][j]+value[k]); k=next[k]; } } int main() { cin>>n>>m; memset(first,-1,sizeof(first)); for (i=1;i<=n-1;i++) { cin>>x>>y>>v; build(x,y,v); build(y,x,v); } dfs(1,0); cout<<f[1][m]<<endl; return 0; }
洛谷P1352 没有上司的舞会
这道就比较简单了
设
f[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值
f[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值
需要注意的是 上司参加了舞会,下属一定不能参加;而上司不参加舞会,下属不是一定要参加舞会,因为数据有负值出现
状态转移方程: f[x][1]+=f[u][0];
f[x][0]+=max(f[u][0],f[u][1]);
#include <bits/stdc++.h> #define maxn 6005 using namespace std; int tot,next[maxn*10],first[maxn],zhi[maxn*10],rong[maxn],i,n,f[maxn][2],l,k; void build (int x,int y) { tot++; next[tot]=first[x]; first[x]=tot; zhi[tot]=y; rong[y]++; } void dfs(int x) { int k,u; k=first[x]; while (k!=-1) { u=zhi[k]; dfs(u); f[x][1]+=f[u][0]; f[x][0]+=max(f[u][0],f[u][1]); k=next[k]; } } int main() { scanf("%d",&n); memset(first,-1,sizeof(first)); for (i=1;i<=n;i++) scanf("%d",&f[i][1]); for (i=1;i<=n;i++) { scanf("%d%d",&l,&k); if (l!=0) build(k,l); } for (i=1;i<=n;i++) if (rong[i]==0) { dfs(i); cout<<max(f[i][0],f[i][1]); } return 0; }
这两道算是最基本的树形dp模型了。
现在来点难的。换根dp
换根dp思想为:
1.随便找一个点作为根进行常规树形dp
2.再以原来点为根进行dp,通过找父亲与孩子之间的关系,寻找孩子节点的答案
换根dp的通法:
1.第一次dfs时,任选一个点为根,在“有根树”上执行一次树形DP,从自底向上的状态转移
2.第二次dfs时,从刚才选出的根出发,对整棵树执行一次dfs,在每次递归前进行自上向底的推导,计算出换根后的解。
这两个都是其他博主总结出来的,笔者觉得很是精辟,可谓一语击中,可以说是把换根dp的重点难点都说出来了。
POJ 3585
这题能说是典型网络流的题目,但是由于题目给的数据大小和给出来的是一棵树,导致只能用树形dp做。
我们先第一遍常规dfs进行dp ,这里找 1为根节点。
这时候来第二遍dfs,我们可以看到对于一个点,如果它有子树的话,那么这个点现有的答案是可以用的,因为第一遍dfs我们求出来每个点的dp值代表了每个点流向子树的最大流量,因此我们只需考虑这个点的“上面”的最大流量是多少(来自父亲节点的最大流量)。
不就可以这么把父亲节点的dp值减去当前节点的dp值,就是“上面”的最大流量吗?
换句话说,父亲节点dp值=左子树最大流量+右子树最大流量。
现在假设当前节点就是父亲节点的右孩子,所以当前节点dp值=右子树最大流量,
我们现在问题是要算可以从“上面”流向的最大流量是多少,那么意思不就是这些流量先经过父亲节点,然后再留过左子树。
因此f[y]=dp[y]+min(f[x]−min(dp[y],len[p]),len[p]);
f
最外层的min是判断一下当前节点流向父亲节点的那条管道是否有超过限制,同理内层的min
#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #define maxn 1000000 using namespace std; int next[maxn],first[maxn],zhi[maxn],value[maxn],du[maxn],i,t,n,x,y,v,maxx,tot,f[maxn],ans[maxn]; void add(int x,int y,int v) { tot++; next[tot]=first[x]; first[x]=tot; zhi[tot]=y; value[tot]=v; du[y]++; } void dp(int x,int father) { int i,u,k; for (k=first[x];k!=-1;k=next[k]) { u=zhi[k]; if (u==father) continue; dp(u,x); if (du[u]!=1) f[x]+=min(f[u],value[k]); if (du[u]==1) f[x]+=value[k]; } } void dfs(int x,int father) { int i,u,k; for (k=first[x];k!=-1;k=next[k]) { u=zhi[k]; if (u==father) continue; if (du[x]==1) ans[u]=f[u]+value[k]; else ans[u]=f[u]+min(ans[x]-min(f[u],value[k]),value[k]); dfs(u,x); } } int main() { cin>>t; while (t--) { cin>>n; tot=0; memset(first,-1,sizeof(first)); memset(f,0,sizeof(f)); memset(du,0,sizeof(du)); for (i=1;i<n;i++) { cin>>x>>y>>v; add(x,y,v); add(y,x,v); } dp(1,0); ans[1]=f[1]; maxx=0; dfs(1,0); for (i=1;i<=n;i++) maxx=max(maxx,ans[i]); cout<<maxx<<endl; } return 0; } /* 2 5 1 2 11 1 4 13 3 4 5 4 5 10 5 1 2 11 1 4 13 3 4 5 4 5 10 */
CCPC Wannafly day3 G
这题难度就上来了,可能先看一下这题先才会更容易理解这里究竟怎么做。AcWing1073
CCPC这题就直接在代码解释。
#include <bits/stdc++.h> #define maxn 1000005 using namespace std; typedef long long ll; ll size[maxn],res[maxn],sum[maxn],Big_max[maxn],Small_max[maxn],up[maxn]; int n,m,tot,txen[maxn],f[maxn],first[maxn],zhi[maxn],value[maxn],fa[maxn],x,y,v,i; void add(int x,int y,int v) { tot++; txen[tot]=first[x]; first[x]=tot; zhi[tot]=y; value[tot]=v; } void dfs(int x,int father) { int k,u; if (f[x]) size[x]=1; for (k=first[x];k!=-1;k=txen[k]) { u=zhi[k]; if (u==father) continue; dfs(u,x); size[x]+=size[u];//size数组意思是以1为根,x的子树中需要旅行的国家数量; sum[x]+=sum[u]; if (size[u]) { sum[x]+=2*value[k];// 对于有要旅行的国家才统计长度 if (Big_max[x]<=Big_max[u]+value[k]) { Small_max[x]=Big_max[x];//选次大的路径 原因看acwing那一题 Big_max[x]=Big_max[u]+value[k];//选最大的路径 fa[x]=u;//记录最大的路径最后能走到哪 } else if (Small_max[x]<Big_max[u]+value[k]) Small_max[x]=Big_max[u]+value[k]; } } } void dp(int x,int father) { int k,u; for (k=first[x];k!=-1;k=txen[k]) { u=zhi[k]; if (u==father) continue; if (m-size[u]) // 如果当前点的子树下面不全包含所有要旅行的国家,那么需要往父亲节点的方向去走 { //up表示以1为根,x通过父亲走到国家的最大值 if (fa[x]==u) up[u]=max(up[x],Small_max[x])+value[k];//如果当前点在以1为根的最长旅行路径上的话,我们需要尝试往父亲节点方向走次大的路径,因为往下走就是刚刚求出来的最长路径 else up[u]=max(up[x],Big_max[x])+value[k]; //当前点不在以1为根的最长旅行路径上的话,肯定是要走最长路径 } res[u]=res[x];//res表示以x为根,到所有国家并返回的路径和; if (size[u]) res[u]-=value[k]*2;//如果下面有要旅行的国家,我们尝试切断与父亲节点的联系 if (m-size[u]) res[u]+=value[k]*2;//如果往父亲节点方向走有要旅行的国家,恢复父亲节点的联系 dp(u,x); } } int main() { scanf("%d%d",&n,&m); memset(first,-1,sizeof(first)); for (i=1;i<n;i++) { scanf("%d%d%d",&x,&y,&v); add(x,y,v); add(y,x,v); } for (i=1;i<=m;i++) cin>>x,f[x]=1; dfs(1,0); res[1]=sum[1]; dp(1,0); for (i=1;i<=n;i++) printf("%lld\n",res[i]-max(up[i],Big_max[i]));//不能保证当前点走次大路径后可能比走最大路径的值会大 return 0; }
对了,对于一个点到所要旅行的所有点的最小路径和,等于他到所有要旅行的点的路径和×2 再减去最长的那条路径的长度。想想为什么呢?顺便吐槽一句,官方题解说是虚树,反正我这里没有一点虚树的做法,我也不会写虚树。