还是很想引用lyd《算法竞赛进阶指南》总的几句话。
在树上设计动态规划时,一般就以节点从深到浅(子树由大到小)的顺序作为dp的阶段。
在方程中一般第一维是节点编号,代表以该节点为根的子树。
大多数情况下都采用递归的形式实现树形dp。
对于每个节点x,先递归它的每个子节点进行dp,在回溯时,从子节点向节点x进行状态转移。
后来翻了翻别人的博客才发现,原来树形dp==套路题,大概就是两个板子倒替用叭QwQ。
两类图:
1 void dfs(int x) 2 { 3 for(int i=first[x];i;i=next[i]) 4 { 5 int y=to[i]; 6 if(vis[y])continue; 7 dfs(y); 8 f[x][0]+=max(f[y][0],f[y][1]); 9 f[x][1]+=f[y][0]; 10 } 11 } 12 int main() 13 { 14 int x,y; 15 cin>>n; 16 for(int i=1;i<=n;i++) 17 cin>>f[i][1]; 18 for(int i=1;i<=n-1;i++) 19 { 20 cin>>x>>y; 21 add(y,x); 22 add(x,y);//注意加两次边,转为无向图 23 } 24 dfs(1);//任选一个点作为根进行dp,这里选择1作为根. 25 cout<<max(f[1][1],f[1][0]); 26 return 0; 27 }
1 void slove(int x) 2 { 3 for(int i=first[x];i;i=next[i]) 4 { 5 int y=to[i]; 6 slove(y); 7 //dp 8 f[x][0]+=max(f[y][0],f[y][1]); 9 f[x][1]+=f[y][0]; 10 } 11 } 12 13 int main() 14 { 15 cin>>n; 16 for(int i=1;i<=n;i++) 17 cin>>f[i][1];//直接读入的时候初始化 18 for(int i=1;i<=n-1;i++) 19 { 20 cin>>x>>y; 21 vis[x]=1; 22 add(y,x); 23 } 24 //找根 25 for(int i=1;i<=n;i++) 26 if(!vis[i]){root=i;break;} 27 28 slove(root); 29 cout<<max(f[root][1],f[root][0]);//输出答案 30 return 0; 31 }
tip:
1.无向图建边两次,需要vis判断是否走过
2.有向图需要找根节点
两类基本方程:
选择节点类
dp[i][0]=dp[j][1]
dp[i][1]=max/min(dp[j][0],dp[j][1])
树形背包类
dp[v][k]=dp[u][k]+val
dp[u][k]=max(dp[u][k],dp[v][k-1])
例题:
1.没有上司的舞会(选择节点类)
一个n个节点的树,每个点有权值Vi,根节点与子节点不能同时取,求总权值的最大值。
1 #include<iostream> 2 #include<vector> 3 using namespace std; 4 int n,f[7000][3]; 5 int vis[7000],h[7000]; 6 vector<int>son[7000]; 7 void dp(int x) 8 { 9 f[x][1]=h[x]; 10 f[x][0]=0; 11 for(int i=0;i<son[x].size();i++) 12 { 13 int y=son[x][i]; 14 dp(y); 15 f[x][0]+=max(f[y][1],f[y][0]); 16 f[x][1]+=f[y][0]; 17 } 18 } 19 int main() 20 { 21 int x,y; 22 cin>>n; 23 for(int i=1;i<=n;i++) 24 cin>>h[i]; 25 while(true) 26 { 27 cin>>x>>y;//y是x上司 28 if(x==0&&y==0)break; 29 vis[x]=1; 30 son[y].push_back(x); 31 } 32 int root; 33 for(int i=1;i<=n;i++) 34 { 35 if(!vis[i]) 36 { 37 root=i; 38 break; 39 } 40 } 41 dp(root); 42 cout<<max(f[root][1],f[root][0]); 43 return 0; 44 }
来分析这个题的话,dp的第一维表示节点状态,可以用01分别代表不选和选。
f[x,1]表示以x为根的子树,并且x选所能产生的最大值。
f[x,0]表示以x为根的子树,并且x不选的最大值。
很显然的动态转移方程:
f[x,0]=max(f[s,0],f[s,1])(s为x的子节点)
f[x,1]=f[s,0]+h[x]
在进行dfs时,要注意标记节点是否走过,以避免遍历过程中沿着反向边回到父节点。
2.最大子树和最大子树和(选择节点类)
很显然的嘛,父节点选的话子节点可以选可以不选,父节点不选子节点肯定不选,那么动态转移方程就很显然了:
f[x][1]=max(f[son][1],f[son][0])
f[x][0]=0
给出优化(美化)之后的代码:
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 int dp[20000],n; 5 int tot=0,first[40000],to[40000],nex[40000],vis[40000]; 6 void add(int x,int y) 7 { 8 tot++; 9 nex[tot]=first[x]; 10 first[x]=tot; 11 to[tot]=y; 12 } 13 void dfs(int x) 14 { 15 vis[x]=1; 16 for(int i=first[x];i;i=nex[i]) 17 { 18 int y=to[i]; 19 if(vis[y])continue; 20 dfs(y); 21 dp[x]+=max(0,dp[y]); 22 } 23 } 24 int main() 25 { 26 int x,y; 27 cin>>n; 28 for(int i=1;i<=n;i++) 29 cin>>dp[i]; 30 for(int i=1;i<n;i++) 31 { 32 cin>>x>>y; 33 add(x,y); 34 add(y,x); 35 } 36 dfs(1); 37 int ans=0; 38 for(int i=1;i<=n;i++) 39 ans=max(ans,dp[i]); 40 cout<<ans; 41 return 0; 42 }
3.选课(树形背包类)
大概就是在一棵树中,点有点权,选子节点时必须选父节点,但是选父节点可以不选子节点的情况下,产生的权值最大。
就是上面提到的背包类型的树形dp,大众方程:
dp[v][k]=dp[u][k]+val
dp[u][k]=max(dp[u][k],dp[v][k-1])
结合代码看一下叭QwQ:
1 #include<iostream> 2 using namespace std; 3 int n,m,tot=0; 4 int first[1100],to[1100],nex[1100],f[1100][1100]; 5 void add(int x,int y) 6 { 7 tot++; 8 nex[tot]=first[x]; 9 first[x]=tot; 10 to[tot]=y; 11 } 12 void dp(int x) 13 { 14 for(int i=first[x];i;i=nex[i]) 15 { 16 int y=to[i]; 17 dp(y); 18 for(int j=m+1;j>=1;j--) 19 for(int k=0;k<j;k++) 20 f[x][j]=max(f[x][j],f[y][k]+f[x][j-k]); 21 } 22 } 23 int main() 24 { 25 int x,y; 26 cin>>n>>m; 27 for(int i=1;i<=n;i++) 28 { 29 cin>>x>>y; 30 add(x,i); 31 f[i][1]=y; 32 } 33 dp(0); 34 cout<<f[0][m+1]; 35 return 0; 36 }
这类问题实际上是背包与树形dp的结合。
F[x,t]表示以x为根的子树中选t门课能获得的最高学分。(就是Gloria意识中的把前i个物品放入容量为j的包里获得的最大价值)
小技巧:一般以第一维态为节点编号,把背包容积作为第二维态,在状态转移时,要处理的实际是分组背包的问题。
浙公网安备 33010602011771号