视频加载失败

树形DP

2448 字
12 分钟
树形DP

树形 DP,顾名思义,就是在树上的 DP.

树形 DP 有以下特征:

  • 转移方程一般从当前点的子树转移而来
  • 转换方程的第一维 n 一般表示以 n 为根的子树,怎么怎么样.
  • 一般来说,树的叶子节点是初始状态,根节点是最终状态
  • 一般会综合所有子节点
  • 树形 DP 的实现类似后序遍历.
  • 树形 DP 和线性 DP 有关联,有时候可以相互转化.

树形 DP 一般只有两个顺序:从上往下(不常见)、从下往上,因此,表达式中不能同时存在子节点信息和父节点信息(即无后效性)

典型案例#

树的大小#

给定一棵树,需要统计树中有多少子节点.

这个问题很简单:

  • 状态定义:dp[u] 表示以 u 为根节点的树中有多少子节点.
  • 初始状态:dp[l]=1,其中 l 是叶子节点.
  • 状态转移:dp[u]=sum(dp[v])+1,其中 vu 的子节点.即 dp[u] 为其所有子节点之和再加上本身.
  • 最终状态:dp[r]r 是根节点.

最大独立集#

题目:P1352 没有上司的舞会

某大学有 nn 个职员,编号为 1n1\ldots n

他们之间有从属关系,员工 kk 是员工 ll 的直接上司,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司.

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 rir_i,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了.

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数.

对于 100%100\% 的数据,保证 1n6×1031\leq n \leq 6 \times 10^3128ri127-128 \leq r_i\leq 1271l,kn1 \leq l, k \leq n,且给出的关系一定是一棵树.

在这里,会用到影响参数化的思想.

  • 状态定义:dp[u][s] 表示以 u 为根节点的树中员工(即员工 u 的间接或直接下属)的最大快乐指数,s 表示员工 u 是(1)否(0)会参加舞会
  • 初始状态:dp[l]=r[l],其中 l 是叶子节点.
  • 状态转移:dp[u][0]=max(dp[v][0],dp[v][1],0)dp[u][0] = max(dp[v][0], dp[v][1], 0)dp[u][1]+=max(dp[v][0],0)+r[u]dp[u][1] += max(dp[v][0], 0)+r[u],其中 vu 的子节点.即 dp[u][0] 为其所有子节点去或不去所得的最大值,dp[u][1] 为其所有子节点不去所得的最大值.
  • 最终状态:max(dp[r][0],dp[r][1])r 是根节点.

标程

#include <bits/stdc++.h>
using namespace std;
const int N = 6e3 + 100;
int n, r[N], dp[N][2], fa[N];
vector<int> e[N];
void dfs(int u) {
for (int i = 0; i < e[u].size(); i++) {
int v = e[u][i];
dfs(v);
dp[u][0] += max(max(dp[v][0], dp[v][1]), 0);
dp[u][1] += max(dp[v][0], 0);
}
dp[u][1] += r[u];
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> r[i];
}
for (int i = 1; i <= n - 1; i++)
{
int l, k;
cin >> l >> k;
e[k].push_back(l);
fa[l] = k;
}
int root = 1;
while (1) {
if (fa[root] == 0) break;
root = fa[root];
}
dfs(root);
cout << max(dp[root][0], dp[root][1]);
return 0;
}

换根 DP#

题目:P3478 STA-Station

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

一个结点的深度之定义为该节点到根的简单路径上边的数量.

对于全部的测试点,保证 1n1061 \leq n \leq 10^61u,vn1 \leq u, v \leq n,给出的是一棵树.

想法一:暴力枚举#

尝试以 O(n)O(n) 枚举每一个点,然后再通过 dfs 统计一遍答案,总计 O(n2)O(n^2)

想法二#

考虑将根上移带来的贡献.

定义:

  • sz[t] 表示在 1 为根节点的树中,以 t 为根节点的子树的节点数量.
  • low[t] 表示在 1 为根节点的树中,以 t 为根的子树中所有节点到节点 t 的距离之和.
  • dp[t] 表示在 t 为根节点的树中,书中所有节点到节点 t 的距离.

sz[t] 很好求,参考树的大小一节.

第一个 dfs 求 low 时,从下到上,有:

low[u]=sz[u]1+uvlow[v]low[u]=sz[u] - 1 +\sum_{u\rightarrow v} low[v]

在上面的式子中:

  • sz[u]1sz[u]-1 表示边 uvu \rightarrow v 这条边所做的贡献.
  • low[v]\sum low[v] 表示其子节点的贡献.

边界条件:无需边界条件,所有节点都是自洽的,即使是根节点.

第二个 dfs 求 dp 时,考虑若将根节点从 u 改成 vvu 的子节点,则有以下变化:

  • 不位于节点 v 下面的节点(不包括 v,共计 nsz[v]n-sz[v] 个节点)每一个到根节点的距离增大 1
  • 位于节点 v 下面的节点(包括 v,共计 sz[v]sz[v] 个节点)每一个到根节点的距离减少 1

从上到下,有:

dp[v]=dp[u]+(nsz[v])(sz[v])dp[v] = dp[u]+(n-sz[v])-(sz[v])

化简一下,就是:

dp[v]=dp[u]+n2sz[v]dp[v]=dp[u]+n-2*sz[v]

考虑边界条件:当 v 是根节点时,dp[v]=low[v]dp[v] = low[v]

整个题的最终状态为:max(dp[t]),tZ,1tnmax(dp[t]),t\in \mathbb{Z},1\leq t\leq n

时间复杂度为 O(n)O(n)

标程:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 100;
int n, f[N], sz[N], vis[N];
long long dp[N];
int low[N];
vector<int> ee[N], e[N];
void build(int u, int fa = 0) {
if (vis[u]) return;
vis[u] = true;
if (fa != 0) e[fa].push_back(u);
for (auto it : ee[u]) {
build(it, u);
}
}
void dfs_sz(int u) {
for (auto it : e[u]) {
dfs_sz(it);
sz[u] += sz[it];
}
sz[u]++;
}
void dfs_1(int u) {
low[u] = sz[u] - 1;
for (auto v : e[u]) {
dfs_1(v);
low[u] += low[v];
}
}
void dfs_2(int u) {
for (auto v : e[u]) {
dp[v] = dp[u] + n - 2 * sz[v];
dfs_2(v);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
ee[u].push_back(v);
ee[v].push_back(u);
}
int root = 1;
build(root);
dfs_sz(root);
dfs_1(root);
dp[root] = low[root];
dfs_2(root);
long long ans = -1, ans_id = -1;
for (int i = 1; i <= n; i++) {
if (ans < dp[i]) {
ans = dp[i];
ans_id = i;
}
}
cout << ans_id;
return 0;
}

树形背包#

题目:P2014 选课

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习.现在有 NN 门功课,每门课有若干学分,分别记作 s1,s2,,sNs_1,s_2,\cdots,s_N,每门课有一门或没有直接先修课(若课程 aa 是课程 bb 的先修课即只有学完了课程 aa,才能学习课程 bb).一个学生要从这些课程里选择 MM 门课程学习,问他能获得的最大学分是多少?

题目保证课程安排无冲突.(即不会有 aabb 的先修课,bb 也是 aa 的先修课这类情况存在.)

对于 100%100\% 数据,保证 1N3001 \leq N \leq 300 , 1M3001 \leq M \leq 300 , 1si201 \leq {s_i} \leq 20

数据保证至少存在一个 ki=0k_i=0,即至少一门课无先修课.

01 背包相类似,定义 dp[t][k] 表示在以 t 为根的子树中,选择 k 门进行学习,所能够获得的最大分数.

由于此图是一个森林,可以将所有没有父节点的节点都归属到节点 0 下,让整个图成为一棵树.相应的,由于必须需要学习课程 0,所以 mm 会增加 1.

此时,初始状态为 dp[t][1]=s[t],tZ,1tndp[t][1]=s[t],t\in \mathbb{Z},1\leq t\leq n,最终状态为 dp[root][m]root 是根节点.

但状态转移方程是什么呢?

我们要将 k 门学习学科的配额进行有效分配.对于每一门学科,又可以分配 [0,n][0,n] 个可学习配额,因此,有:

dp[u][k]=max(uvdp[v][ai])+s[u],0aik,uvai=k1dp[u][k] = \max(\sum_{u\rightarrow v} dp[v][a_i]) + s[u],0\leq a_i\leq k,\sum_{u\rightarrow v}a_i=k-1

,即:

  • 对于每一种分配方案,为每一个子节点分配一定的可学习科目额度 aia_i,所有可学习科目额度之和恰好等于 k1k-1(这是由于节点 uu 必修,如果不修节点 uu,则无法学习其子节点).
  • 对于所有分配方案,取其中收益最大的.

那要通过超高的时间复杂度来枚举每一种情况并计算吗?

我们可以引入一个辅助数组 g[m][k],表示已经考虑 mu 的子节点,且使用了 k 个学习配额,所能够获得的最大收益.

则有初始状态 g[0][1]=s[u]g[0][1]=s[u],即只选择根节点 uu 时的情况.

不难发现状态转移方程:g[m][H]=g[m1][A]+dp[vm][HA],0AHg[m][H] = g[m-1][A]+dp[v_m][H-A],0\leq A\leq H,其实就是在增加一个子节点时,枚举这个子节点所得的配额 AA,用一种类似打擂台的方法统计出结果.使用完 g 数组后一定要清空!!!

利用 g 数组来优化 dp 的状态转移方程:

k,dp[u][k]=g[vmax][k]\forall k,dp[u][k] = g[v_{max}][k]

vmaxv_{max} 为节点 uu 的子节点数量.必须选取节点 uu,因为如果不选取节点 uu,节点 uu 中的所有子节点都无法被选取.时间复杂度 O(n2)O(n^2)

标程:

#include <bits/stdc++.h>
using namespace std;
const int N = 305;
const int INF = 0x3f3f3f3f;
int n, m, s[N], root;
vector<int> e[N];
int dp[N][N], g[N][N], sz[N];
void dfs_sz(int u) {
sz[u] = 1;
for (int v = 0; v < e[u].size(); v++) {
dfs_sz(e[u][v]);
sz[u] += sz[e[u][v]];
}
}
void dfs(int u) {
for (int v : e[u])
dfs(v);
int son_cnt = e[u].size();
// 初始化 g 为 -INF
for (int i = 0; i <= son_cnt; ++i)
for (int j = 0; j <= m; ++j)
g[i][j] = -INF;
g[0][1] = s[u];
int tot_sz = 1;
for (int i = 0; i < son_cnt; ++i) {
int v = e[u][i];
for (int j = 1; j <= tot_sz; ++j) {
if (g[i][j] == -INF)
continue;
// 不选 v
g[i + 1][j] = max(g[i + 1][j], g[i][j]);
// 选 v 的 k 门课
for (int k = 1; k <= sz[v]; ++k) {
if (dp[v][k] == -INF)
continue;
int nj = j + k;
if (nj > m)
break;
g[i + 1][nj] = max(g[i + 1][nj], g[i][j] + dp[v][k]);
}
}
tot_sz += sz[v];
}
for (int j = 1; j <= min(tot_sz, m); ++j)
dp[u][j] = g[son_cnt][j];
}
int main() {
cin >> n >> m;
m++;
for (int i = 1; i <= n; i++) {
int k;
cin >> k >> s[i];
dp[i][1] = s[i];
e[k].push_back(i);
}
for (int i = 1; i <= n; i++) {
for (int j = m; j <= m; j++) {
dp[i][j] = -INF;
}
}
dfs_sz(0);
dfs(0);
cout << dp[0][m] << endl;
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

树形DP
https://blog.jerrylab.top/posts/count/tree-dp/
作者
Jerry
发布于
2026-03-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Jerry
Hello, I'm Jerry.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
80
分类
3
标签
35
总字数
89,198
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0