树形DP选讲

动态规划的重点就是找出正确的dp顺序/关系

而树形结构中严格的父子关系极其方便

树dp从数据结构角度来讲 可以说是 利用dp避免了做每个子树的答案的时候要遍历整个子树

从状态来讲

"树 dp 常常需要设计几种意义不同的状态,而且状态的含义通常与该点为根的子树有关。" --By Yukimai

 

树 dp 的转移通常有两种方式,自底向上 ( 即从儿子转移到父亲 ) 和自顶向下 ( 从父亲转移到儿子 ) 。

这两种转移的复杂度都是 O(n)* 单次转移复杂度,因为每条边只会被考虑一次。

不论哪种转移方式都可以一次 dfs 实现。

在掌握了树和dp之后组合进阶还是比较容易的

下面算是几道经典题

 

T1 最大独立集 luoguP1352 没有上司的舞会  传送门

这个题算是基础 明显的最优化 无后效性 树结构

更新的时候与正常的dp一样 在dfs的时候回溯并用儿子的值做父亲的答案

f[i]表示选i时  以i为根节点的子树产生贡献的大小

g[i]表示不选i时 以i为根节点的子树产生贡献的大小

所以可以自然更新

其实这道题会一点dp就能做出来

Code:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<queue>
 6 #include<vector>
 7 #define ms(a,b) memset(a,b,sizeof a)
 8 #define rep(i,a,n) for(int i = a;i <= n;i++)
 9 #define per(i,n,a) for(int i = n;i >= a;i--)
10 #define inf 2147483647
11 using namespace std;
12 typedef long long ll;
13 ll read() {
14     ll as = 0,fu = 1;
15     char c = getchar();
16     while(c < '0' || c > '9') {
17         if(c == '-') fu = -1;
18         c = getchar();
19     }
20     while(c >= '0' && c <= '9') {
21         as = as * 10 + c - '0';
22         c = getchar();
23     }
24     return as * fu;
25 }
26 const int N = 100005;
27 //head
28 int n,rt;
29 int val[N];
30 int head[N],nxt[N],mo[N],cnt;
31 void add(int x,int y) {
32     mo[++cnt] = y;
33     nxt[cnt] = head[x];
34     head[x] = cnt;
35 }
36 
37 bool flag[N];
38 int f[N],g[N];
39 void dfs(int x) {
40     f[x] = 0;
41     g[x] = val[x];
42     for(int i = head[x];i;i = nxt[i]) {
43     int sn = mo[i];
44     dfs(sn);
45     g[x] += f[sn];
46     f[x] += max(g[sn],f[sn]);
47     }
48 }
49 
50 int main() {
51     n = read();
52     rep(i,1,n) val[i] = read();
53     while(1) {
54     int x = read();
55     int y = read();
56     if(!x && !y) break;
57     add(y,x);
58     flag[x] = 1;
59     }
60     while(flag[++rt]);
61     dfs(rt);
62     printf("%d
",max(f[rt],g[rt]));
63     return 0;
64 }
View Code

 

T2  最小覆盖

(1)luoguP2016 战略游戏 传送门

起手先分一下状态

f[i]表示这个点有士兵 g[i]表示没有 下的 合法情况 的最优答案

采用自下而上的顺序的话比较好更新

所以根据定义 这个点不放 还合法 只能是它所有的儿子都有士兵(否则x->sn就空了)

所以g[x] = ∑(x->sn∈E)f[sn];

同理 如果i点有士兵 那么儿子放不放都行

f[x] = ∑(x->sn∈E)min(f[sn],g[sn]);

最后的答案依然是max(f[root],g[root])

Code:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<queue>
 6 #include<vector>
 7 #define ms(a,b) memset(a,b,sizeof a)
 8 #define rep(i,a,n) for(int i = a;i <= n;i++)
 9 #define per(i,n,a) for(int i = n;i >= a;i--)
10 #define inf 2147483647
11 using namespace std;
12 typedef long long ll;
13 ll read() {
14     ll as = 0,fu = 1;
15     char c = getchar();
16     while(c < '0' || c > '9') {
17         if(c == '-') fu = -1;
18         c = getchar();
19     }
20     while(c >= '0' && c <= '9') {
21         as = as * 10 + c - '0';
22         c = getchar();
23     }
24     return as * fu;
25 }
26 const int N = 3005;
27 //head
28 int n,rt;
29 int val[N];
30 int head[N],nxt[N],mo[N],cnt;
31 void _add(int x,int y) {
32     mo[++cnt] = y;
33     nxt[cnt] = head[x];
34     head[x] = cnt;
35 }
36 void add(int x,int y){if(x^y)_add(x,y),_add(y,x);}
37 
38 int f[N],g[N];
39 void dfs(int x,int fa) {
40     f[x] = 1,g[x] = 0;
41     for(int i = head[x];i;i = nxt[i]) {
42     int sn = mo[i];
43     if(sn == fa) continue;
44     dfs(sn,x);
45     g[x] += f[sn];
46     f[x] += min(g[sn],f[sn]);
47     }
48 }
49 
50 int main() {
51     n = read();
52     rep(i,1,n) {
53     int x = read() + 1;
54     int k = read();
55     rep(j,1,k) {
56         int y = read() + 1;
57         add(x,y);
58     }
59     }
60     dfs(1,1);
61     printf("%d
",min(f[1],g[1]));
62     return 0;
63 }
View Code

(2)luoguP2279 [HNOI2003]消防局的设立 传送门

上面那个是简单情况

考虑步数大于1

多开几倍空间? 很抱歉是错的。

多开空间能处理根节点还能向上延伸几步

但是无法考虑经过父节点更新其他兄弟的情况

考虑最小覆盖只求总结果 而且每次操作只有++

完全可以考虑dp[]表示其他情况

所以用dp[x]表示x向上还能延伸几步

如果为负 则表示需要上面多远的节点延伸几步

dfs回溯的时候记录所有儿子里面向上延伸步数的最大值和最小值

Code:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<queue>
 5 #define ms(a,b) memset(a,b,sizeof a)
 6 #define rep(i,a,n) for(int i = a;i <= n;i++)
 7 #define per(i,n,a) for(int i = n;i >= a;i--)
 8 #define inf 1000000007
 9 using namespace std;
10 typedef long long ll;
11 typedef double D;
12 #define eps 1e-8
13 ll read() {
14     ll as = 0,fu = 1;
15     char c = getchar();
16     while(c < '0' || c > '9') {
17         if(c == '-') fu = -1;
18         c = getchar();
19     }
20     while(c >= '0' && c <= '9') {
21         as = as * 10 + c - '0';
22         c = getchar();
23     }
24     return as * fu;
25 }
26 const int N = 100005;
27 //head
28 int n,ans;
29 int head[N],nxt[N],mo[N],cnt;
30 void add(int x,int y) {
31     mo[++cnt] = y;
32     nxt[cnt] = head[x];
33     head[x] = cnt;
34 }
35 
36 int dp[N];
37 void dfs(int x) {
38     int minn = 10,maxx = -10;
39     for(int i = head[x];i;i = nxt[i]) {
40         int sn = mo[i];
41         dfs(sn);
42         minn = min(minn,dp[sn]);
43         maxx = max(maxx,dp[sn]);
44     }
45     if(minn == 10) dp[x] = -1;
46     //trick,没有儿子一定不如父亲放
47     else if(minn <= -2) ans++,dp[x] = 2;
48     //有节点不能被已有覆盖
49     //注意不会出现未被覆盖的情况
50     else if(maxx + minn > 0) dp[x] = maxx - 1;
51     //最大的儿子可以覆盖所有的儿子
52     else dp[x] = minn - 1;
53     //目前有节点不能被覆盖 但是可以向上继续决策
54 }
55 
56 int main() {
57     n = read();
58     rep(i,2,n) add(read(),i);
59     dfs(1);
60     // rep(i,1,n) printf("%d ",dp[i]);
61     ans += (dp[1] < 0);
62     //修正trick的边界
63     printf("%d
",ans);
64     return 0;
65 }
View Code

T3 树上背包 luoguP2014 选课

树上背包题目比较多 这里还是举最基础的例子

(其实还有个名字叫有依赖背包?)

具体就是 选儿子必须选祖先

依然是dfs处理 

dp[i][j]表示i为根的子树中花费为j的最大价值

回溯的时候仿照01背包猜dp方程就是

dp[i][j] =  max( dp[i][j] , dp[i][k] + dp[sn][j-k] );

但是复杂度O(n*v^2) 

考虑优化 复杂度瓶颈在于每次要枚举子树的花费和总花费

利用之前的思路 能不能用儿子的状态代替整个子树的状态

发现 儿子如果不选 它的子树都不能选

考虑dfs序 同时可以处理出子树大小

在前序遍历上面做的话 子树的所有儿子一定在儿子的后面且大小是sze[sn]

转移方程就比较简单了qwq

dp[i][j] = max( dp[i + 1][j - cst[ dfs[i] ] + val[ dfs[i] ]  ,  dp[i + sze[i]][j] );

前一个表示选这个子树 后一个不选

正序遍历dfs序就ok 复杂度O(n*v)

注意要dfs的时候初始化

而且 必须有一个明确的dfs序 即有根树

Code:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<cmath>
 5 #include<queue>
 6 #include<vector>
 7 #define itn int
 8 #define ms(a,b) memset(a,b,sizeof a)
 9 #define rep(i,a,n) for(int i = a;i <= n;i++)
10 #define per(i,n,a) for(int i = n;i >= a;i--)
11 #define inf 2147483647
12 using namespace std;
13 typedef long long ll;
14 ll read() {
15     ll as = 0,fu = 1;
16     char c = getchar();
17     while(c < '0' || c > '9') {
18         if(c == '-') fu = -1;
19         c = getchar();
20     }
21     while(c >= '0' && c <= '9') {
22         as = as * 10 + c - '0';
23         c = getchar();
24     }
25     return as * fu;
26 }
27 const int N = 3005;
28 //head
29 int n,m;
30 int head[N],nxt[N],mo[N],cnt;
31 void add(int x,int y) {
32     mo[++cnt] = y;
33     nxt[cnt] = head[x];
34     head[x] = cnt;
35     return;
36 }
37 
38 int val[N],sze[N],dp[N][N];
39 
40 void dfs(int x) {
41     sze[x] = 1;
42     for(int i = head[x];i;i = nxt[i]) {
43         dfs(mo[i]);
44         sze[x] += sze[mo[i]];
45     }
46     for(int i = head[x];i;i = nxt[i]) {
47         int sn = mo[i];
48         per(j,sze[x],1) rep(k,1,j)
49             dp[x][j] = max(dp[x][j],dp[x][j-k] + dp[sn][k]);
50     }
51     per(i,sze[x],0) dp[x][i+1] = dp[x][i] + val[x];
52     return;
53 }
54 
55 int main() {
56     n = read();
57     m = read();
58     rep(i,1,n) {
59         int x = read();
60         val[i] = read();
61         add(x,i);
62     }
63     dfs(0);
64     printf("%d
",dp[0][m+1]);
65     return 0;
66 }
View Code

 

 

T4 最优连通块问题

(I) 给定一棵无向树,树上每个点有权值,求一个权值和最大的连通块并输出该权值和。

Sol:

开始的时候会想到分成选根和不选根两种情况

但是会发现如果根不选是没有必要维护的 也不能更新答案

所以dp[i]表示以i为根的子树(必须选i)的最大价值

显然 dp[i] = w[i] + ∑max(0,dp[sn]) (sn->i∈E)

由于是无向树 答案就是所有dp中最大的 

而且只用搜一遍 复杂度O(n)

 

(II) 给定一棵树,要求删去最少的边,得到一个恰好有 k 个点的连通块

luoguP1272 传送门

Sol:

按照上一问的思路 dp[i][j]表示以i为根大小为j最少删多少边

儿子同时具有大小和删多少边两个属性 考虑背包

dp[i][j] = min(dp[i][j],dp[i][k] + dp[sn][j-k] - 2)(i->sn∈E)

初始化dp[i][1] = 儿子数+1

由于i->sn在dp[i][k]dp[sn][j-k]中考虑了两次 而我们最后还要加上 所以-2

最后所有点 i 求max(dp[i][k]) 就行

复杂度O(n*V*V)

Code:

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<queue>
 5 #define ms(a,b) memset(a,b,sizeof a)
 6 #define rep(i,a,n) for(int i = a;i <= n;i++)
 7 #define per(i,n,a) for(int i = n;i >= a;i--)
 8 #define inf 1000000007
 9 using namespace std;
10 typedef long long ll;
11 typedef double D;
12 #define eps 1e-8
13 ll read() {
14     ll as = 0,fu = 1;
15     char c = getchar();
16     while(c < '0' || c > '9') {
17         if(c == '-') fu = -1;
18         c = getchar();
19     }
20     while(c >= '0' && c <= '9') {
21         as = as * 10 + c - '0';
22         c = getchar();
23     }
24     return as * fu;
25 }
26 //head
27 const int N = 1005;
28 int n,m,p;
29 int head[N],nxt[N<<1],mo[N<<1],cnt;
30 int du[N];
31 void _add(int x,int y) {
32     du[x]++;
33     mo[++cnt] = y;
34     nxt[cnt] = head[x];
35     head[x] = cnt;
36 }
37 void add(int x,int y) {if(x^y)_add(x,y),_add(y,x);}
38 
39 int dp[N][N],res;
40 void dfs(int x,int f) {
41     dp[x][1] = du[x];
42     for(int i = head[x];i;i = nxt[i]) {
43         int sn = mo[i];
44         if(sn == f) continue;
45         dfs(sn,x);
46         per(j,p,1) rep(k,1,j)
47             dp[x][j] = min(dp[x][j],dp[x][j-k] + dp[sn][k] - 2);
48     }
49     res = min(res,dp[x][p]);
50 }
51 
52 int main() {
53     n = read(),p = read();
54     ms(dp,60),res = inf;
55     rep(i,2,n) add(read(),read());
56     dfs(1,1);
57     printf("%d
",res);
58     return 0;
59 }
View Code

 

 

 

T5 最大收益问题

给定一棵有根树,第 i 个节点上有 w[i] 价值的宝物 ( 只能被获取一 次 ) ,每通过第 i 条边一次就会消耗 c[i] 的费用,现在从根节点开始,每次可移动到相邻的节点上 ( 并付出相应的代价及获得相应的收益 ) ,可以在任意节点停止,求最大收益。

考虑路径可能的情况

无非是进一个子树再出来或者进一个子树不出来了

所以 f[i] 表示进 i 为根的子树不出来答案

g[i]表示进 i 为根的子树再出来的答案

由于需要子树的答案 所以回溯的时候更新

求f的时候枚举进去的儿子sn

f[i] = max(f[i] , w[i] - cst[fa[i] -> i] + f[sn] + ∑( i -> k ∈ E && k != j ) g[k] );

求g的时候就不用枚举了

g[i] = max(0,w[i] - cst[fa[i]->i] * 2 + ∑(i -> j ∈ E) g[j]);

初值全是0就Ok

这里发现 枚举sn的时候有一个 其他子树权值和 会吃复杂度

所以可以在更新之前预处理一遍所有子树的g[sn]和

至此 大部分知识点就BB完了

 

----Updated on 18.11.01----- Thank to mrclr

> 别忘了 总有人在等着你
原文地址:https://www.cnblogs.com/yuyanjiaB/p/9752485.html