zoukankan      html  css  js  c++  java
  • 树形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

    > 别忘了 总有人在等着你
  • 相关阅读:
    数据的安装及常识
    数据库基础
    zabbix入门
    zabbix安装报错
    https
    rewrite
    keepalived
    计算机键盘在编辑情况下键盘不见,仅能输入字母
    集成学习
    js
  • 原文地址:https://www.cnblogs.com/yuyanjiaB/p/9752485.html
Copyright © 2011-2022 走看看