【算法笔记】树形DP算法总结详解

0. 定义

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

1. 基础

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

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

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

【例1.1】子树大小

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

本题明显可以使用树形DP的方法,令\(f[v]=~\)\(v\)为根的子树大小,则易得

\[f[v]=1+\sum_{i=1}^{\text{deg}_v} G[v][i] \]

即:一个结点的子树大小\(~=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 没有上司的舞会

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

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

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

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

  • 不选\(v\)时,可选也可不选\(u\),此时有\(g(v)=\sum\max\{f(u),g(u)\}\)
  • 选择\(v\)时,一定不能选\(u\),此时有\(f(v)=r_i+\sum g(u)\)

时间复杂度为\(\mathcal 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 选课

\(N\)门课,第\(i\)门课的学分是\(s_i\)每门课有不超过一门先修课,需要上了先修课才能上这门课。现要选\(M\)门课,使得学分总和最大。

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

格式问题解决,下面考虑如何\(\text{DP}\)
\(f[i][j]\)表示当前在结点\(i\)、且已经选了\(j\)门课时的最大学分数量,则答案为\(f[0][M+1]\)。状态转移方程等详见代码。时间复杂度为\(\mathcal 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,时间复杂度一般为\(\mathcal O(N)\)

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

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

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

#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;
}

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

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

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

\(f_u\)表示以\(u\)为根时,所有结点的总深度和,则\(f_1=\sum\text{depth}_i\)
考虑\(f_u\to f_v\)的转移,即“根结点从\(u\)变成\(v\)时所有结点深度和的变化”,则有:

  • 所有在\(v\)的子树上的结点深度全部\(-1\),则总深度和减少\(s_v\)
  • 所有不在\(v\)的子树上的结点深度都\(+1\),则总深度和增加\(n-s_v\)

此时,可得\(f_v=f_u-s_v+n-s_v=f_u+n-2s_v\)。注意数据类型,使用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;
}
AC

习题

  • 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 - 博客园

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/794253.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Seay安装和初步使用

作者网站现在已经无法访问:http://www.cnseay.com/2951/, 可以使用这个GitHub - f1tz/cnseay: Seay源代码审计系统 下载完安装包之后,解压到自己想要的电脑路径即可,无需进行任何额外的配置 利用工具对sql注入进行分析 进入软件之后,点击新建项目,选择需要分析的文件(这里…

纸浆

日线: 小时线: 15分钟趋势: 关注5750一线支撑

代码整洁之道--读书笔记(5)

代码整洁之道简介: 本书是编程大师“Bob 大叔”40余年编程生涯的心得体会的总结,讲解要成为真正专业的程序员需要具备什么样的态度,需要遵循什么样的原则,需要采取什么样的行动。作者以自己以及身边的同事走过的弯路、犯过的错误为例,意在为后来者引路,助其职业生涯迈上更…

02网络参考模型

02网络参考模型02网络参考模型常见网络模型因为 OSI协议栈比较复杂 ,且TCP和IP两大协议在业界被广泛使用,所以 TCP/IP参考模型 成为了互联网的主流参考模型。OIS网络模型层级作用7.应用层 应用层 对应用程序提供接口。6.表示层进行数据格式的转换,以确保一个系统生成的应用层…

《痞子衡嵌入式半月刊》 第 107 期

痞子衡嵌入式半月刊: 第 107 期这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。 本期刊是开源项目(GitHub: JayHeng/pzh-mcu-bi-weekly),欢迎提交 issue,投稿或推荐你知道的嵌入式那些事儿。 上期回顾 :《…

鼠标悬停显示的轮播图

今日整理,发现这种轮播图是最难实现的一种, 1.再循环中难以控制单一品类商品显示 解决办法: 在外面的主类里面添加&:hover触发标签属性的更改,这样可以单一作用 2.在循环中触发事件,所有的同一事件都会触发 解决办法:先建立模版控制排版,再从单一内容开始微调 <script s…

如何把网页的公式优雅地拷贝到word中:数学公式识别神器—Mathpix Snip

这个编辑器其实在把chatgpt的公式粘贴到word中时就已经使用了,用的是网页版。 现在下载了软件(但是好像一个月试用期过后得收费?但是就目前来说,体验感真的超级好) 把公式复制粘贴转成mathtype公式 可以截取电脑屏幕上的图像,如果图像上面有公式的话,就会识别,之后可以…

Redis 入门 - 图形化管理工具如何选择,最全分类

Redis图形化管理工具可分为四类:命令行工具、桌面客户端工具、网页工具、插件工具。看看哪一款适合你呢?工欲善其事必先利其器,上一章Redis服务环境已经搭建完成,现在就需要一个趁手的工具,有个好工具可以做到事半功倍。 Redis图形化管理工具五花八门,可供选择的很多,大…

Javaweb-事务

注意在当前窗口是修改了的:而在其他窗口是不修改的:select @@autocommit;修改为手动提交:

格路计数

前言 完全借鉴于 洛谷日报 前提条件 在一个网格图上, 每一次可以从 \((x,y)\) 走到 \((x+1,y-1)\) 或者 \((x+1,y-1)\) 限制是一条直线 \(y=k\) 题外话 我们其实可以发现这和每一次可以从 \((x,y)\) 走到 \((x+1,y)\) 或者 \((x, y+1)\) 限制是一条 \(45\)的斜线 \(y=x+k\) 是等…

9.2-9.8 总结

zhicheng123456做题 因为联考众多,所以说只有不多的做题。主要得知了两个结论:找树剖到根的轻链(动态)的方法和把线段(均匀随机分界点)分为 \(n\) 段的 \(k\) 短值的计算。 联考:https://www.cnblogs.com/british-union/p/liankao.html 还是时常在简单的东西上失败,但是…

第一章预习作业

第一周预习报告 学习内容《WindowsC/C++加密解密实战》第1,2章 第一章概念复习 第二章主要在Linux(Ubuntu,openEuler)上把软件更新到最新版(3.0版本以上),如果默认不是,参考下面脚本。 AI对学习内容的总结 要求 让AI(kimi,元宝等)阅读学习内容并进行总结,教材内容可使…