树型DP
DFS的回溯是树形DP的重点以及核心,当回溯结束后,root的子树已经被遍历完并处理完了。这便是树形DP的最重要的特点
自己认为应该注意的点
- 好多人都说在更新当前节点时,它的儿子结点都给更新完了,实际上这并不准确。对于当前节点,我们需要dfs它的儿子,并且在dfs中进行dp。在此过程中并不是等到儿子都更新完我们才更新当前节点的信息(假设当前节点为x, 有儿子son1 , son2, son3, 且son1已经更新完了, 即x已有了son1的信息, son2刚刚更新完,即dfs正在son2的位置回溯), 我们拿着son2, x和son1的信息再次更新x, 如此,x才有了son1,son2的综合信息,之后再从son3 dfs进去找son3的信息,最后才得到x的信息。
- 需要注意枚举的顺序,树型dp有点像01背包,而01背包更新信息时你如果没有记录对于i,i中前j个的状态,你就需要倒序枚举,而树型dp中通常直接用f[x] [...]...表示x所在子树的信息,这里枚举k的时候就需要像01背包一样了,从size[x]逆序枚举。
(01背包倒着写时要倒序,不然就表示可以多次使用前面的物品更新后面的物品的状态(比如你顺着写,你第j个物品用到了前j个物品来更新,那当你再用第j个物品更新第j+1个物品时,又把前面的算了一遍,所以就算重复了),这不就成了完全背包嘛,我们的01背包倒着写是为了每次更新,都是取用它前面的状态,而它前面的状态又是没改过的,所以不会选重。树型dp同理,用前j个儿子更新第j+1个儿子时不能选重复)
梨提
luoguP1352 没有上司的舞会(熟悉一下回溯)
https://www.luogu.org/problemnew/show/P1352
/*
f[i] [0/1] 0与1分别表示第i个人不去,去时的最大快乐指数
所以 f[i] [0] += max( f[son] [1] ,f[son] [0]);
f[i] [1] += max(f[son] [0] , 0)
为什么f[i][1] 要与0 做比较? : 因为快乐指数可能是负的
为什么f[i][0] 不用与0作比较,直接=max?:就算是负的,也只能直接去最大的,他又不去...
为什么是+=? : 下面有
*/
#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 6000+9; int n,cnt; int head[MAXN]; int R[MAXN],f[MAXN][2],fa[MAXN]; struct edge{ int y,next; }e[MAXN]; void add_edge(int x, int y) { e[++cnt].y = y; e[cnt].next = head[x]; head[x] = cnt; } void dfs(int now) { f[now][1] = R[now]; f[now][0] = 0;//初始化 for(int i = head[now]; i; i = e[i].next ) {//i是边编号 int nn = e[i].y ; dfs(nn);//先递归进去处理儿子的f值 f[now][0] += max(f[nn][1] ,f[nn][0]) ; f[now][1] += max(f[nn][0] , 0); //注:因为一棵树可能有多个儿子,所以这里都是+=; } } int main() { scanf("%d",&n); for(int i = 1; i <= n; i++) { scanf("%d",&R[i]); } int l,k; for(int i = 1; i <= n; i++) { scanf("%d%d",&l,&k); if(i == n) break;//只输入了n-1行 fa[l] = k; add_edge(k,l); } int root; for(int i = 1; i <= n; i++) if(!fa[i]) { root = i; break;//找根 } dfs(root); int ans = max(f[root][0],f[root][1]); printf("%d",ans); }
luoguP2014 选课(注意树型dp要符合dp的特点)
https://www.luogu.org/problem/P2014
注意枚举当前节点所选的课要倒序枚举,原因同上
#include<cstdio> #include<algorithm> using namespace std; const int MAX = 300+9; int n,m; int f[MAX][MAX], arr[MAX], size[MAX]; //f[i][j]表示子树i中(包括i)选j门课的最大学分 struct edge{ int y, next; }e[MAX<<1]; int head[MAX], cnt; void add_edge(int x, int y) { e[++cnt].y = y; e[cnt].next = head[x]; head[x] = cnt; } void dfs(int x) { size[x] = 1; for(int i = head[x]; i; i = e[i].next) { dfs(e[i].y); size[x] += size[e[i].y]; for(int k = size[x]; k >= 1; k--) {//父亲不选就都不能选,所以>=1//这儿的k必须倒序 for(int j = 0; j < k; j++) {//枚举在当前儿子中选的课 f[x][k] = max(f[x][k],f[x][k-j] + f[e[i].y][j]); } } } } int main() { scanf("%d%d",&n,&m); int x; for(int y = 1; y <= n; y++) { scanf("%d%d",&x,&arr[y]); add_edge(x,y); } m++; // f[0][1] = 0; for(int i = 1; i <= n; i++) f[i][1] = arr[i]; dfs(0); printf("%d",f[0][m]); }
luoguP2015 二叉苹果树
注: 因为相当于0-1背包中的选或不选,所以 j 是逆序的,其他细节在代码里有体现和解释
https://www.luogu.org/problemnew/show/P2015
#include<cstdio> #include<algorithm> using namespace std; const int MAXN = 100+9; int n,Q; int f[MAXN][MAXN];//f[i][j]表示: 点i和它的子树保留j个树枝时的最大苹果数 int head[MAXN],cnt; struct edge{ int y,val,next; }e[MAXN]; void add_edge(int x, int y, int val) { e[++cnt].y = y; e[cnt].val = val; e[cnt].next = head[x]; head[x] = cnt; } void dfs(int now ,int dad) { f[now][0] = 0;//初始化边界 for(int i = head[now]; i; i = e[i].next ) { int nn = e[i].y ; if(nn == dad) continue ;//应该是continue吧,不是return ; dfs(nn, now);//先递归进去处理儿子的f值 for(int j = Q; j; j--) {//逆序的原因: 0-1背包选或不选 for(int k = 0; k < j; k++) {//枚举 左/右 子树保留的树枝 f[now][j] = max(f[now][j], f[now][j-k-1] + f[nn][k] + e[i].val ); //要选子树nn上边,就要把子树nn与根的边选上,所以这里是j-k还要"-1" } } } } int main() { scanf("%d%d",&n,&Q); int m = n-1;//二叉树的边 for(int i = 1, x, y, val; i <= m; i++) { scanf("%d%d%d",&x,&y,&val); add_edge(x,y,val); add_edge(y,x,val);//只是描述了边,但不知道父亲是谁,儿子是谁,所以建双向的 //所以下面的dfs要开一个树根的形参,防止死循环 } //1为根 dfs(1,1); printf("%d",f[1][Q]); return 0; }