0. 定义

树形DP,又称树状DP,即在树上进行的DP,是DP(动态规划)算法中较为复杂的一种。

1. 基础

令f[u]=f[u]=~f[u]= 与树上顶点uuu有关的某些数据,并按照拓扑序(从叶子节点向上到根节点的顺序)进行DP\text{DP}DP,确保在更新一个顶点时其子节点的dp值已经被更新好,以更新当前节点的DP\text{DP}DP值。为方便计算,一般写成dfs的形式,如下:

void dfs(int v) { // 遍历节点vdp[v] = ...; // 初始化for(int u: G[v]) { // 遍历v的所有子节点dfs(u);update(u, v); // 用子节点的dp值对当前节点的dp值进行更新}
}

下面来看一道简单的例题:

【例1.1】子树大小

给定一棵有NNN个结点的树,根结点为结点111。对于i=1,2,…,Ni=1,2,\dots,Ni=1,2,…,N,求以结点iii为根的子树大小(即子树上结点的个数,包括根结点)。

本题明显可以使用树形DP的方法,令f[v]=f[v]=~f[v]= 以vvv为根的子树大小,则易得
f[v]=1+∑i=1degvG[v][i]f[v]=1+\sum_{i=1}^{\text{deg}_v} G[v][i] f[v]=1+i=1∑degv​​G[v][i]
即:一个结点的子树大小=1~=1 =1(根节点)++~+ 每个子树的大小。

沿用刚才的模板,可得:

#include <cstdio>
#include <vector>
#define maxn 100
using namespace std;vector<int> G[maxn]; // 邻接表
int sz[maxn]; // dp数组,sz[v] = 子树v的大小void dfs(int v)
{sz[v] = 1; // 初始化,最初大小为1,后面累加for(int u: G[v]) // 遍历子结点{dfs(u); // 先对子结点进行dfssz[v] += sz[u]; // 更新当前子树的大小}
}int main()
{int n;scanf("%d", &n); // 结点个数for(int i=1; i<n; i++) // N-1条边{int u, v;scanf("%d%d", &u, &v); // 读入一条边G[u].push_back(v); // 存入邻接表}dfs(1);for(int i=1; i<=n; i++)printf("%d\n", sz[i]);return 0;
}

下面来看一道稍微复杂一点的题:

【例1.2】洛谷P1352 没有上司的舞会

本题即树的最大独立集问题。

有NNN名职员,编号为1…N1\dots N1…N,他们的关系就像一棵以老板为根的树,父节点就是子节点的直接上司。每个职员有一个快乐指数rir_iri​,现在要召开一场舞会,使得没有职员和直接上司一起参会。主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

令f(v)f(v)f(v)表示以vvv为根的子树中,选择vvv的最优解,g(v)g(v)g(v)表示以vvv为根的子树中,不选vvv的最优解。

则对于每个状态,都存在两种决策(其中uuu代表vvv的儿子):

  • 选择vvv时,可选也可不选uuu,此时有g(v)=∑max⁡{f(u),g(u)}g(v)=\sum\max\{f(u),g(u)\}g(v)=∑max{f(u),g(u)};
  • 不选vvv时,一定不能选uuu,此时有f(v)=ri+∑g(u)f(v)=r_i+\sum g(u)f(v)=ri​+∑g(u)。

时间复杂度为O(N)\mathcal O(N)O(N)。
注意本题需要寻找根节点,没有上司的结点即为根节点,读入时用数组标记即可。

#include <cstdio>
#include <vector>
#define maxn 6005
using namespace std;inline int max(int x, int y) { return x > y? x: y; }vector<int> G[maxn]; // 邻接表
bool bad[maxn]; // 根结点标记
int f[maxn], g[maxn]; // 数据存储void dfs(int v) // 遍历结点v
{// 读入时已初始化,这里可省略for(int u: G[v]) // 遍历子结点{dfs(u); // 先对子结点进行dfs// 更新当前dp状态f[v] += g[u]; // 选择v,不能选ug[v] += max(f[u], g[u]); // 不选v,u可选可不选}
}int main()
{int n;scanf("%d", &n); // 结点个数for(int i=0; i<n; i++)scanf("%d", f + i); // 相当于提前初始化好f[i]=r[i]for(int i=1; i<n; i++) // N-1条边{int u, v;scanf("%d%d", &u, &v); // 读入一条边G[--v].push_back(--u); // 0-index,存入邻接表bad[u] = true; // 标记不可能是根结点}int root = -1; // 根结点变量for(int i=0; i<n; i++)if(!bad[i]) // 找到根结点{root = i; // 记录根结点break;}dfs(root); // 开始进行树形DPprintf("%d\n", max(f[root], g[root])); // 根结点也有两种选择return 0;
}

习题

  • HDU 2196 Computer / vjudge链接
  • POJ 1463 Strategic game
  • 洛谷 P3574 [POI2014] FAR-FarmCraft

2. 树上背包

在基本算法之上,树形dp还可以用于树上背包问题。来看一道例题:

【例2.1】洛谷P2014 / AcWing 286 选课

有NNN门课,第iii门课的学分是sis_isi​。每门课有不超过一门先修课,需要上了先修课才能上这门课。现要选MMM门课,使得学分总和最大。

每门课最多只有一门先修课,这符合树结构的特点,与有根树中一个点最多只有一个父亲结点的特点类似。因此,我们根据数据构造一棵树,课程的先修课为这门课的父结点。又由于给定的输入是一个森林(多棵树组成的不一定连通的图),不是一棵完整的树,因此我们添加虚拟根结点000(s0=0s_0=0s0​=0),将没有先修课的结点全部连到它下面,并从这里开始dfs。注意此时必须选中000号结点(它是所有课程的直接或间接先修课),所以操作前先将MMM加上111。

格式问题解决,下面考虑如何DP\text{DP}DP。
令f[i][j]f[i][j]f[i][j]表示当前在结点iii、且已经选了jjj门课时的最大学分数量,则答案为f[0][M+1]f[0][M+1]f[0][M+1]。状态转移方程等详见代码。时间复杂度为O(NM)\mathcal O(NM)O(NM),有兴趣的可以自己尝试证明。

#include <cstdio>
#include <vector>
#include <algorithm>
#define maxn 305
using namespace std;// dp算法中常用的模板,等效于x=max(x,y)
inline void setmax(int& x, int y)
{if(x < y) x = y;
}vector<int> G[maxn]; // 邻接表
int n, m, f[maxn][maxn];int dfs(int u) // 遍历结点u,返回值为其子树大小
{int tot = 1; // 记录子树大小,初始为1for(int v: G[u]) // 遍历u的所有子结点{int sz = dfs(v); // 对当前子结点进行搜索// 状态转移,注意i倒序,防止串连转移现象for(int i=min(tot, m); i>0; i--) // 子树大小优化可降低算法复杂度for(int j=1, lim=min(sz, m-i); j<=lim; j++)setmax(f[u][i + j], f[u][i] + f[v][j]); // 更新状态tot += sz; // 加到当前子树下}return tot; // 返回子树大小
}int main()
{scanf("%d%d", &n, &m);for(int i=1; i<=n; i++){int a;scanf("%d%d", &a, f[i] + 1); // 初始化f[i][1]=s[i]G[a].push_back(i);}m ++; // 别忘了这一句dfs(0);printf("%d\n", f[0][m]);return 0;
}

习题

  • LOJ #2546. 「JSOI2018」潜入行动
  • LOJ #2268. 「SDOI2017」苹果树

3. 换根 DP

换根DP,即为不知道根结点时使用的一种树形DP,时间复杂度一般为O(N)\mathcal O(N)O(N)。

【例3.1】洛谷 P3478 [POI2008] STA-Station

给定一个nnn个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

先考虑最简单粗暴的方法,即为枚举所有结点,代码如下:

#include <cstdio>
#include <vector>
#define maxn 1000005
using namespace std;vector<int> G[maxn];int dfs(int v, int d, int par)
{int s = d;for(int u: G[v])if(u != par)s += dfs(u, d + 1, v);return s;
}int main()
{int n;scanf("%d", &n);for(int t=n; --t; ){int u, v;scanf("%d%d", &u, &v);G[--u].push_back(--v);G[v].push_back(u);}int ans = 0, maxDepth = dfs(0, 0, -1);for(int root=1; root<n; root++){int d = dfs(root, 0, -1);if(d > maxDepth) ans = root, maxDepth = d;}printf("%d\n", ++ans);return 0;
}

很明显,这种做法时间复杂度为O(n2)\mathcal O(n^2)O(n2),又因为n≤106n\le 10^6n≤106,所以无法得全分,评测结果如下:

好家伙,居然还有50分,本以为最多30…

下面来考虑换根DP的方法。不妨令uuu为当前结点,vvv为其子结点。先预处理出每个结点的子树大小s[u]=1+∑s[v]s[u]=1+\sum s[v]s[u]=1+∑s[v]和以111为根结点时所有结点的深度(depthi\text{depth}_idepthi​),此时第一遍DFS即为预处理。

令fuf_ufu​表示以uuu为根时,所有结点的总深度和,则f1=∑depthif_1=\sum\text{depth}_if1​=∑depthi​。
考虑fu→fvf_u\to f_vfu​→fv​的转移,即“根结点从uuu变成vvv时所有结点深度和的变化”,则有:

  • 所有在vvv的子树上的结点深度全部−1-1−1,则总深度和减少svs_vsv​;
  • 所有不在vvv的子树上的结点深度都+1+1+1,则总深度和增加n−svn-s_vn−sv​;

此时,可得fv=fu−sv+n−sv=fu+n−2svf_v=f_u-s_v+n-s_v=f_u+n-2s_vfv​=fu​−sv​+n−sv​=fu​+n−2sv​。注意数据类型,使用long long

#include <cstdio>
#include <vector>
#define maxn 1000005
using namespace std;using LL = long long;vector<int> G[maxn];
LL sz[maxn], f[maxn];
int n, ans;LL dfs1(int v, int d, int par)
{sz[v] = 1;LL s = d;for(int u: G[v])if(u != par)s += dfs1(u, d + 1, v), sz[v] += sz[u];return s;
}void dfs2(int v, int par)
{if(f[v] > f[ans]) ans = v;for(int u: G[v])if(u != par){f[u] = f[v] + n - (sz[u] << 1LL);dfs2(u, v);}
}int main()
{scanf("%d", &n);for(int t=n; --t; ){int u, v;scanf("%d%d", &u, &v);G[--u].push_back(--v);G[v].push_back(u);}f[0] = dfs1(0, 0, -1);dfs2(0, -1);printf("%d\n", ++ans);return 0;
}

习题

  • POJ 3585 Accumulation Degree
  • 洛谷 P2986 [USACO10MAR] Great Cow Gathering G
  • CodeForce 708C Centroids
  • ABC 222F - Expensive Expense

4. 后记

好像这玩意也并不是开头所说的那么难…… 记得给个三连哦!

参考文献:

  • 树形 DP - OI wiki
  • 树形dp - tom0727’s blog
  • 【动态规划】树形DP完全详解! - RioTian - 博客园

【算法笔记】树形DP算法总结详解相关推荐

  1. 数据结构与算法笔记:图搜索之DFS详解

    图搜索Graph Search的分类 BFS广度优先(宽搜) DFS深度优先(深搜) !!!本文详解!!! 深度优先搜索DFS 深度优先遍历DFS, 这个策略其实是非常stupid or simple ...

  2. 0x54. 动态规划 - 树形DP(习题详解 × 12)

    目录 0x54.1 树形DP Problem A. 没有上司的舞会 Problem B. 战略游戏 0x54.2 树上背包 Problem A. 选课 Problem B.[数据加强版]选课(树上背包 ...

  3. 蓝桥杯 试题 算法训练 无聊的逗 C++ 详解 - 未完善

    题目: 逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中.不过他想到了一个游戏来使他更无聊.他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的 ...

  4. JAVA中希尔排序去的讲解_java 中基本算法之希尔排序的实例详解

    java 中基本算法之希尔排序的实例详解 希尔排序(Shell Sort)是插入排序的一种.也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本.希尔排序是非稳定排序算法.该方法因DL.Shel ...

  5. EM算法(Expectation Maximization Algorithm)详解

    EM算法(Expectation Maximization Algorithm)详解 主要内容 EM算法简介 预备知识  极大似然估计 Jensen不等式 EM算法详解  问题描述 EM算法推导 EM ...

  6. 蓝桥杯 试题 算法训练 无聊的逗 C++ 详解

    题目: 逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中.不过他想到了一个游戏来使他更无聊.他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的 ...

  7. 【JVM】对象存活判定算法、GC算法、STW、GC种类详解

    [JVM]对象存活判定算法.GC算法.STW.GC种类详解 文章目录 [JVM]对象存活判定算法.GC算法.STW.GC种类详解 GC主要关注的区域 垃圾标记阶段:对象存活判断 标记阶段:引用计数算法 ...

  8. 【目标检测算法-锚框公式推导及代码详解】

    目标检测算法-锚框公式推导及代码详解 0 沐神对锚框的宽高计算并未推导以及讲解 1 锚框宽高公式推导 1.1 基础概念 1.2 锚框宽高公式推导 1.3 图片验证计算 1.4 小结 2 代码详解 2. ...

  9. 大白话解析Apriori算法python实现(含源代码详解)

    大白话解析Apriori算法python实现(含源代码详解) 一.专业名词解释 二.算法思路 三.python代码实现 四.Aprioir的优点.缺点及改进方法 本文为博主原创文章,转载请注明出处,并 ...

  10. (转)dp动态规划分类详解

    dp动态规划分类详解 转自:http://blog.csdn.NET/cc_again/article/details/25866971 动态规划一直是ACM竞赛中的重点,同时又是难点,因为该算法时间 ...

最新文章

  1. USACO 2.3 货币系统(背包/生成函数)
  2. project template
  3. Hadoop学习之Combiner
  4. Android 8.0 学习(14)---Android8.0适配分析
  5. Linux异常 时间戳 2018-10-08 11:17:22 是未来的 5288025.776562967 秒之后
  6. 实时全局光照Screen Space Reflection (SSR)
  7. linux用独显运行steam,修复在Linux系统上与Nvidia不兼容的Steam游戏
  8. 微软笔试题《Arithmetic Puzzles》- 题解
  9. 5G物联网数据网关助力工业企业转型升级
  10. html 隐藏广告代码,Javascript实现关闭广告实现删除广告的效果
  11. .net接入微信二维码支付(模式二)
  12. 关于阿里矢量图标的普通无色和彩色的使用方法
  13. 极化码:极化码的单项式码(Monomial Codes)表示
  14. qtxlsx编译报错_qt5.12搭建qtxlsx库读取excel表格编译错误和解决方法第二讲
  15. Java程序员11面阿里,错失offer,期间还面了EMC+网易+美团......
  16. Factorization Machines 因式分解机 论文学习笔记
  17. main()的报错——疯狂递(而不)归
  18. from表单点击submit提交后没有反应
  19. 2016c语言模拟试卷A,2016C语言模拟试卷(读程序写结果).doc
  20. 软件测试工程师的待遇怎么样

热门文章

  1. 7-7 词典 (15 分)
  2. html5拾色器功能,html5 学习简单的拾色器
  3. 微信小程序中slider实现拾色器功能
  4. 不用任何软件!PDF转Word用微信这个功能,简单又方便!
  5. ssis oracle配置,[SSIS][Oracle]安裝 Oracle Driver 提供 SSIS 使用
  6. 创新检查技术,赋能保密监管 ,您需要一款这样的数据库内容保密检查系统!
  7. windows10如何查看硬盘序列号
  8. 在html css中加粗显示,HTML和CSS实现字体加粗的方法有哪些
  9. h5.v2.php,最新H5影视双端PHP源码 可封装APP
  10. 寻星时卫星数字电视接收机的信号检测功能