【动态规划】树形DP完全详解!
蒟蒻大佬时隔三个月更新了!!拍手拍手
而且是更新了几篇关于DP的文章(RioTian狂喜)
现在赶紧复习一波树形DP....
在学习树形DP之前,我们先要搞清楚一个问题,什么是树?根据图论课上学到的知识我们知道,连通的无圈图称为树。而树我们可以把它近似第看成一个分形结构,这是说我们的树其实是可以递归定义的,树的每个子树也是一颗完整的树,而这种结构就天然地适合递归。
具体来说,在树形动态规划当中,我们一般先算子树再进行合并,在实现上与树的后序遍历相似,都是先遍历子树,遍历完之后将子树的值传给父亲。简单来说我们动态规划的过程大概就是先递归访问所有子树,再在根上合并。
了解了树形动态规划的基本思想后,做一些经典的树形DP题型
【经典例题】
【子树大小】
Description
给你一棵有
这道题作为我们树形动态规划的引例,当然是非常简单的,只要跑一遍
具体细节:
-
设
以 为根的子树大小,则 ,其中 为 子节点。如果写成伪码,大概长这个样子
void dfs(u){
if (u 是叶子) f[u] = 1 return
for (v 是 u 的儿子){
dfs(v)
f[u] += f[v]
}
f[u] += 1 // 本身
}
这就是最简单的树形DP了,有没有感受到先遍历子树,遍历完之后将子树的值传给父亲这样的动态规划过程呢?
【树的平衡点】
Description
给你一个有
要解决这个问题,我们首先还是先确定状态,现在的问题还很简单,一般就是题目问什么我们就设什么,所以我们先设
通俗的来讲,即删除某个节点后,它的儿子就成了独立的连通块,那么最大连通块就是 max(x 的所有儿子连通块最大的 size,n - f[x])
借用一下蒟蒻dalao的图
所以我们只需要先沿用第一题的方法先算出每个点的子树大小,再
【代码实现】
vector<int>e[N];
int ans, idx, f[N];
void dfs(int u, int fa) {
f[u] = 1;
int mx = 0;
for (int v : e[u]) {
if (v == fa)continue;
dfs(v, u);
f[u] += f[v];
mx = max(mx, f[v]);
}
mx = max(mx, n - f[u]);
if (ans > mx) ans = mx, idx = u;
}
没有上司的舞会 (树的最大独立集)
Description
有
这个在 树形DP基础 里讲过了
我们把题意抽象一下,没有职员会和上司一同参会,也就是说在这棵树上不存在任何一条边使得连接的两个点都来参会,换句话说这道题其实要我们求的是 树的最大权值的独立集。
蒟蒻大佬在他的博客提到了——— 黑白染色,也就是在树上一层选一层不选这样的贪心。
但其实很容易证明简单的黑白染色是不对的,如下图所示,左边是黑白染色的结果,右边是正解的结果。其中黑点表示要来参加的人,白点表示不来参加的人,在这个例子中我们默认每个人的快乐指数都是一样的。
所以我们只好老老实实地去确定状态一步一步来。我们发现
转移方程:
- 当选
去参加舞会时,它的儿子不能参加,所以
其中我们的边界条件就是当
我们最后的答案就是
而如果转变为树的最大独立集就只要让我们的
【AC Code】
const int N = 1e4 + 10;
vector tr[N];
int f[N][2], v[N], Happy[N], n;
void dfs(int u) {
f[u][0] = 0; f[u][1] = Happy[u];
for (auto v : tr[u]) {
dfs(v);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> Happy[i];
for (int i = 1, x, y; i < n; ++i) {
cin >> x >> y;
v[x] = 1;// x has a father
tr[y].push_back(x);
}
int root;
for (int i = 1; i <= n; ++i)
if (!v[i]) {root = i; break;}
dfs(root);
cout << max(f[root][0], f[root][1]) << "\n";
}
Strategic game (树的最小点覆盖)
Description
给你一个有

这道题和上一道题: 没有上司的舞会 (树的最大独立集) 挺像的,题目链接:Here
都是能够发现
那我们的状态就很明朗了,
- 设
为不在 号点上放士兵并且以 为根的每条边都被看住的最小士兵数; - 设
为在 号点上放士兵并且以 为根的子树的每条边都被看住的最小士兵数。
接下来我们来考虑转移,
-
当
点不放士兵时,它的儿子就必须都要放士兵,所以 ; -
当
点不放士兵时,它的儿子可以放士兵,也可以不放士兵,所以
我们最后的答案就是:
这就是树的最小点覆盖问题的解法。
vector<int>e[N];
int f[N][2];
bool st[N];
void dfs(int u) {
st[u] = 1;
f[u][0] = 0, f[u][1] = 1;
for (int i = 0; i < e[u].size(); ++i) {
int v = e[u][i];
if (st[v]) continue;
dfs(v);
f[u][0] += f[v][1];
f[u][1] += min(f[v][0], f[v][1]);
}
}
int main() {
int n, m, k, x;
while (~scanf("%d", &n)) {
for (int i = 1; i <= n; ++i) {
scanf("%d:(%d)", &m, &k);
while (k--) {
scanf("%d", &x);
e[m].push_back(x), e[x].push_back(m);
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (st[i]) continue;
dfs(i);
ans += min(f[i][0], f[i][1]);
}
printf("%d\n", ans);
memset(st, 0, sizeof(st));
for (int i = 0; i < N; ++i) e[i].clear();
}
}
Cell Phone Network (树的最小支配集)
题目链接:Here
Description
给你一个有

这道题可以说是上两道题的升级版了,之前两道题一个点选或者不选,只会影响它的儿子选或者不选;但是在这道题当中,一个点选或者不选不仅会影响儿子还会影响父亲,所以在状态设计上会更加复杂一点点。
-
设
表示选点 ,且以 为根的子树每个点都被覆盖的最少信号塔部署数量; -
设
表示不选点 ,并且 被儿子覆盖的最少信号塔部署数量; -
设
为不选 ,但是 没被儿子覆盖,且以 为根的子树的其他点都被覆盖的最少信号塔部署数量,换句话说这时
的父亲一定要选来覆盖一下 。
下面我们来看看如何进行转移。
-
当我们选
点时, 点的儿子可选可不选,并且可以已经被覆盖和未被覆盖,所以我们有 -
当我们不选点
,并且 未被覆盖的时候,它的儿子们都至少被覆盖或者不选,不能不选又不被覆盖,所以这个时候
最难的情况是当我们不选点
- 如果这时
存在一个儿子 ,有 ,也就说选了它不会更劣,那么我们就选它,我们的答案回归 - 如果不存在这样一个儿子,也就是说对于所有儿子,选了都会变得更劣,那我们就要记录一下
, 也就是选一个损失最小的来选,我们的答案就变成 .
这题有点复杂我下面展示一下大致的参考代码:
void dfs(int x){
v[x] = 1;
f[x][0] = 1,f[x][1] = f[x][2] = 0;
int tmp = inf;
bool flag = 1;
for(int i = 0;i < e[x].size();++i){
int y = e[x][i];
if(vis[y]) continue;
dfs(y);
f[x][2] += min(f[y][1],f[y][0]);
f[x][0] += min(f[y][0],f[y][1],f[y][2]); // 重载min
if(f[y][0] <= f[y][1]){
flag = 0;
f[x][1] += f[y][0];
} else{
f[x][1] += f[y][1];
tmp = min(tmp,f[y][0] - f[y][1]);
}
}
if(flag) f[x][1] += tmp;
}
// 简化思路的AC代码
const int N = 1e4 + 10, inf = 0x3f3f3f3f;
vector<int>e[N];
int ans;
bool vis[N];
void dfs(int u, int fa) {
bool f = 0;
for (int i = 0; i < e[u].size(); ++i) {
int v = e[u][i];
if (v == fa) continue;
dfs(v, u); f |= vis[v];
}
if (!f && !vis[u] && !vis[fa])
vis[fa] = 1, ans += 1;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n; cin >> n;
for (int i = 1, x, y; i <= n; ++i) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1, 0);
cout << ans;
}
到这里我们的最小支配集问题也得到了解决。
【二叉苹果树】树上背包问题
Description
有一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共

在刚学树形DP的时候遇到过,当时没能解决
题目链接:Here
首先这道题规定苹果是长在树枝上的,所以我们不妨设
表示连接 和其左儿子树枝上的苹果数量 表示连接 和其右儿子树枝上的苹果数量
我们在这里由于最后要求在给定需要保留的树枝数量时最多能留住苹果的数量,所以我们不妨直接设
-
首先是对于
,它的左右子树都保留的情况,这时以 为根的子树保留 个树枝,我们再设左右子树分别保留 和 个树枝,那么我们有 ,所以我们就枚举 x,这时 ,所以这时情况一 -
接着是对于只保留左子树的情况,这时以
为根的子树保留 个树枝,所以左子树要保留 个树枝,所以这时
- 其次是对于只保留右子树的情况,这时以
为根的子树保留 个树枝,所以右子树也要保留 个树枝,所以这时
- 最后是左右子树都不保留的情况,这时
,也就是 。
这样分析完
我们先开一个虚构的左子树,如下图三角形,然后我们让第一个子树当作右子树和我们虚构的左子树合并,并将合并完的结果当成一个左子树再喝第二个子树合并,并将合并完的结果当作一个左子树和第三个子树合并……以此类推。而其中的合并就是我们上面二叉树枚举
具体写法的转移过程大概是这样的:
这其实就是一个树上背包的思想,希望同学们能够用心体会一下。
【树的直径】
另一篇关于树的直径的文章详细见这里:Here,练习:Here
Description
给你一个有
搜索基础好的同学可能已经看出来了,其实不用 DP ,两遍 DFS 也能解决问题。具体来说,我们从树上任意一个点以起点出发,找到一条最长的边然后以这条边的另一个顶点作为起点再 DFS 一遍找到的最长边就是我们树的直径。
那一遍 DFS ,从一个点出发找一条第一长的找一条第二长的加起来行不行呢?那肯定是不行的,还是这个例子,我们选一个第一长的一个第二长的,就发现我们经过了一条公共边,这当然是不允许的。
那这个两遍 DFS 为什么是对的呢,我们下面来感性证明一下。
首先我们先抽线一下我们的过程,我们就是先从一点出发找到离他最远的另一点,再从找到的那一点出发找一条从它出发的最长的链。
首先如果我们第一步找到的点在我们的最长链上,那答案一定是对的。下面我们证明我们第一步找到的点一定在我们的最长链上,若不然,我们分两种情况进行讨论:
- 首先是真实的最长链和我们求出的最长链有交点,并相交在第一步求出的最长链上的情况。假如最长链如下图蓝色所示,假若它长于我们找到的黑色。首先由于我们第一遍找的是根开始的最长链,所以
,而第二遍我们是从找到的那一点出发找的最长链,所以 ,所以 ,这与假设的蓝色才是最长链 矛盾!
- 接着是真实的最长链和我们求出的最长链有交点,并相交在第二步求出的最长链上的情况。假如最长链如下图蓝色所示,假若它长于我们找到的黑色。首先由于我们第一遍找的是根开始的最长链,所以
,而第二遍我们是从找到的那一点出发找的最长链,所以 ,所以 ,这与假设的蓝色才是最长链 矛盾!
-
最后就是真实的最长链和我们求出的最长链没有交点的情况。假如最长链如下图蓝色所示,假若它长于我们找到的黑色。不失一般性,我们不妨设
,这时我们另一条链也就是我们橙色这条的长度为 ,由于我们的 是从根出发的最长链,所以 ,而 ,所以 ,这与
是最长链相矛盾!从另一个角度我们也能分析出矛盾,我们知道 是从根出发的最长链,所以 ,而 ,而不妨设 ,此时至少有 ,所以 ,所以 ,这与 是从我们第一次搜索找到的点出发的最长链相矛盾!
综上所述两遍 DFS 的方法的正确性是无可否认的。
搞定了 DFS 后,接下来我们考虑我们如何使用树形动态规划的方法来解决这个问题。首先我们知道我们最后的答案一定是如我们那个抽象的图一样是从一个低点上升到最高点再下降的,而上升下降两条边一定是这个最高点的两条最长边。我们由此得到启示,我们设
当然其实不是点权而是边权的情况也能轻松完成,毕竟点权和边权是可以通过点边转换来互相转换的。
Rinne Loves Edges
Description
定义删除一条边的代价为这条边的边权,现在
4月份做每日一题时写的题解:Here
首先我们发现这是一个
所以我们就可以顺势设出
这道题到这也轻松的解决了。我们下面来考虑一下这道题的一个变型,下面是题目描述:
Description
定义删除一条边的代价为这条边的边权,由于能力有限,切断边的总代价不能超过
题目链接:Here
首先有一个很容易想到的错误做法,就是我们把边进行排序,然后从小的开始删。但因为有总长度限制,这种删法又可能会删除一些多余的边,比如断开一个子树时,这个子树里面已经有被我删的边了,就可能达不到它能力
一看到最大值最小,是不是 DNA 就动起来了,没错就是二分的思想。我们去二分能切的最长的边的代价,这时我们就把我们的边分成了能切断的和不能切断的。这时我们跑上面的那个动态规划,假设这时二分到
我们最后看看答案
最后还有两道题分别是:HDU 3586 和 POJ 2152
这里稍微提一下思路,
HDU 3586:最小化最大值(二分),对上限进行二分,用树形DP去判断;
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
2020-08-19 A*(A star)搜索总结
2020-08-19 Codeforces Round #629 (Div. 3) & 19级暑假第六场训练赛
2020-08-19 线性代数(1):矩阵以及运用
2020-08-19 __builtin_popcount() 函数
2020-08-19 【洛谷日报#26】GCC自带位运算系列函数