什么是树形 DP
树形动态规划是把动态规划思想应用到树结构上:利用树的层次与递归结构,把”整棵树的最优解”分解为”子树的最优解”逐层合并。它几乎总是通过 DFS 实现,复杂度一般为 O(n)。
树形 DP 的三个特征:
- 层次性:树天然分层,适合自底向上(先子后父)或自顶向下(先父后子)递推;
- 递归性:子树是独立子问题,可以递归求解后合并;
- 无后效性:父节点状态只依赖子节点状态,满足 DP 的无后效性要求。
两种遍历方向
叶 → 根(后序遍历)
先递归处理子节点,再用子节点信息更新父节点——树形 DP 的主流形式:
void dfs(int u, int parent) {
for (int v : tree[u]) {
if (v == parent) continue;
dfs(v, u); // 先算子树
// 用 dp[v] 更新 dp[u]
}
}
根 → 叶(先序遍历)
先处理父节点,再把信息向下传递给子节点,常用于”换根 DP”等需要父亲影响儿子的场景:
void dfs(int u, int parent) {
// 用父节点传来的信息初始化 dp[u]
for (int v : tree[u]) {
if (v == parent) continue;
dfs(v, u); // 把信息传给子节点
}
}
经典问题
1. 没有上司的舞会(最大独立集)
选择若干节点,要求不能同时选相邻节点,求选中节点价值和最大。
状态:dp[u][0] 不选 u 的最大值;dp[u][1] 选 u 的最大值。转移:
void dfs(int u, int parent) {
dp[u][1] = value[u]; // 选 u
for (int v : tree[u]) {
if (v == parent) continue;
dfs(v, u);
dp[u][0] += max(dp[v][0], dp[v][1]); // u 不选:子节点随便
dp[u][1] += dp[v][0]; // u 选了:子节点必须不选
}
}
2. 树的直径
求树上任意两点间的最长路径。对每个节点记录 d1[u](向下的最长链)和 d2[u](次长链),答案就是全局最大的 d1[u] + d2[u]:
void dfs(int u, int parent) {
for (auto [v, w] : tree[u]) {
if (v == parent) continue;
dfs(v, u);
int len = d1[v] + w;
if (len > d1[u]) { d2[u] = d1[u]; d1[u] = len; }
else if (len > d2[u]) { d2[u] = len; }
}
diameter = max(diameter, d1[u] + d2[u]);
}
3. 树上背包(选课问题)
课程有依赖关系(先修课),选 m 门课使总学分最大。设 dp[u][j] 为以 u 为根的子树中选 j 门课的最大值,用子树做”分组背包”合并,容量倒序保证每棵子树最多贡献一次:
void dfs(int u) {
dp[u][1] = credit[u]; // 先修课本身占一门
for (int v : tree[u]) {
dfs(v);
for (int j = m; j >= 1; j--) // 容量倒序
for (int k = 1; k < j; k++)
dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
}
}
解题套路
- 状态设计:一般用
dp[u][k]表示”以 u 为根的子树 + 附加状态 k”的最优解;附加状态常用来表达父子约束(选了没有、颜色、是否在子树内等); - 存储:用邻接表或链式前向星,注意传入父节点防止回访;
- 森林处理:多棵树时加一个虚拟根节点统一 DFS;
- 换根 DP:先算出以 1 为根的答案,再通过”父子树贡献回传”在第二次 DFS 中求出所有节点为根时的答案,是树上题的高频进阶技巧。
常见变种
树的重心、树的最小点/边覆盖、树的匹配、树上的最大权独立集(去掉相邻限制的变体)等,核心都是”定好状态、子树合并”。
