树形DP进阶:让动态规划在树上“生根发芽”

引言

动态规划(DP)是算法竞赛中最灵活也最强大的工具之一。当DP遇到树这种数据结构时,就诞生了树形DP------在树上做动态规划。

树形DP的基础模型很简单:从叶子节点向上递推,用子节点的信息更新父节点。但实际题目往往不会这么直接------"求树上距离每个节点最远的节点""在树上做背包选课""每个节点都可以作为根,求全局最优"......这些进阶问题需要更精巧的DP设计。

本文将从树形DP的基础出发,重点讲解两个进阶方向:树上背包 (在树上做分组背包)和换根DP(让每个节点都成为根,求全局最优)。

如果把基础树形DP比作"从树叶到树根收集信息",那么树上背包就是 "在收集信息时还要做选择" ,而换根DP则是 "不仅要从下往上收集,还要从上往下传递" 。三种能力合在一起,才能应对树上DP的全部挑战。


前置知识

在学习树形DP进阶之前,你需要掌握以下基础:

  1. 树的基本概念:节点、边、根、子树、叶子节点。

  2. 深度优先搜索(DFS) :树形DP的核心遍历方式。

  3. 基础树形DP:如"没有上司的舞会"------每个节点选或不选,用子节点更新父节点。

  4. 背包DP:0/1背包、分组背包的基本概念。

  5. 递归与记忆化:树形DP通常用递归实现。


第一章:树上背包------在树上"选课"

1.1 问题引入:有依赖的背包

普通的背包问题是:有若干个物品,每个物品有重量和价值,在容量限制下选择物品使总价值最大。

树上背包 在此基础上增加了一个约束:物品之间存在依赖关系------要选择某个物品,必须先选择它的父节点。

最经典的例子是"选课":每门课可能有先修课,要选"数据结构"必须先选"程序设计基础"。如果把课程之间的依赖关系画成一棵树,问题就变成了"在树上选择若干节点,必须选父节点才能选子节点"。

1.2 状态设计

dp[u][j] 表示:在以节点u为根的子树中,选择j个节点(且必须选择u本身)所能获得的最大价值。

关键约束 :因为必须选u才能选子树中的其他节点,所以 dp[u][j]j 至少为1(至少选u自己)。

1.3 转移方程

对于节点u,我们逐个处理它的每个子节点v,像做分组背包一样,把子树的"选课方案"当作一组物品:

text

复制代码
初始化 dp[u][1] = val[u](只选u自己)
对于每个子节点 v:
    对于 j 从 m 到 1(当前已选的课程数,倒序):
        对于 k 从 1 到 size[v](在v的子树中选k门课):
            dp[u][j+k] = max(dp[u][j+k], dp[u][j] + dp[v][k])

代码框架

cpp

复制代码
void dfs(int u, int fa) {
    sz[u] = 1;
    dp[u][1] = val[u];  // 只选自己
    for (int v : children[u]) {
        if (v == fa) continue;
        dfs(v, u);
        // 背包合并:倒序枚举容量
        for (int j = min(m, sz[u]); j >= 1; j--) {
            for (int k = 1; k <= sz[v] && j + k <= m; k++) {
                dp[u][j+k] = max(dp[u][j+k], dp[u][j] + dp[v][k]);
            }
        }
        sz[u] += sz[v];
    }
}

1.4 复杂度分析

树上背包的时间复杂度是 O(n·m²) 在最坏情况下,但实际运行中,由于每次合并两个子树,总复杂度通常为 O(n·m) (每个节点对在容量维度上被考虑一次)。

1.5 经典例题:洛谷 P2014 选课

题目描述:有n门课,每门课有学分,有些课有先修课(最多一门)。在最多选m门课的条件下,如何选课使总学分最大?

解题思路

  1. 构建树:每门课是一个节点,先修课是父节点。

  2. 如果有多棵树(森林),添加一个虚拟根节点0,学分0,将森林变成一棵树。

  3. 运行树上背包,答案就是 dp[0][m+1](因为虚拟根节点也占了一门课)。

核心代码(来自):

cpp

复制代码
// dp[x][j]:在以x为根的子树中选j门课的最大学分
// 转移:将子节点y的子树作为一个分组,做分组背包
for (int j = m; j >= 0; j--) {
    for (int k = 1; k <= sz[y]; k++) {
        if (j + k <= m) {
            dp[x][j+k] = max(dp[x][j+k], dp[x][j] + dp[y][k]);
        }
    }
}

第二章:换根DP------让每个节点都成为"根"

2.1 问题引入:不定根的最优解

很多树形DP问题会问:"对于树上的每个节点,求以该节点为根时的某个答案"。

最朴素的做法是:枚举每个节点作为根,分别做一次树形DP,时间复杂度 O(n²) 。当n达到10⁵时,这显然不可行。

换根DP(也称二次扫描法) 通过两次DFS 将复杂度优化到 O(n)

  1. 第一次DFS:任选一个根(如节点1),计算以它为根的答案。

  2. 第二次DFS:利用父节点的答案推导出子节点作为根时的答案。

如果把基础树形DP比作"从下往上递推",换根DP就是 "先从上往下推一次,再从下往上推一次,两次信息合在一起才是完整答案"

2.2 换根DP的核心思想

换根DP的关键是理解:当根从父节点u转移到子节点v时,答案会发生什么变化?

对于大多数问题,变化只涉及两部分:

  • v的子树:所有节点的深度都减少1(因为根下移了一层)。

  • v的子树之外:所有节点的深度都增加1(因为离根远了一层)。

如果能快速计算出这两个部分的贡献,就能在O(1)时间内从 ans[u] 推导出 ans[v]

2.3 经典例题:洛谷 P3478 STA-Station

题目描述 :给定一棵n个节点的树,求一个节点,使得以该节点为根时,所有节点的深度之和最大

解题思路

第一次DFS(计算基础信息)

  • 任选1为根。

  • 计算每个节点的子树大小 sz[u]

  • 计算以1为根时的深度之和 sum[1]

第二次DFS(换根转移)

  • 假设已知以u为根的深度之和 sum[u]

  • 当根从u转移到子节点v时:

    • v的子树(大小 sz[v])中所有节点的深度都减少1

    • 其他节点(数量 n - sz[v])的深度都增加1

  • 因此转移公式为:

sumv=sumu−szv+(n−szv)sumv=sumu−szv+(n−szv)

化简得:

sumv=sumu+n−2×szvsumv=sumu+n−2×szv

代码框架

cpp

复制代码
void dfs1(int u, int fa) {
    sz[u] = 1;
    for (int v : children[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        sum[1] += sz[v];  // 以1为根时,每条边对深度的贡献
    }
}

void dfs2(int u, int fa) {
    for (int v : children[u]) {
        if (v == fa) continue;
        sum[v] = sum[u] + n - 2 * sz[v];  // 换根转移
        dfs2(v, u);
    }
}

最终,sum[i] 就是以i为根时的深度之和,取最大值对应的节点就是答案。

2.4 换根DP的通用模板

换根DP的通用思路可以总结为:

  1. 第一次DFS:计算每个节点的子树信息(大小、贡献等)。

  2. 第二次DFS :从父节点向子节点转移,用 ans[fa] 推导 ans[child]

思考换根DP时,问自己两个问题

  • 根节点的答案如何计算?(第一次DFS)

  • 怎么将答案从父亲传向儿子?(第二次DFS)


第三章:树形DP进阶的综合应用

3.1 树上背包 + 换根DP

有些题目会同时用到树上背包和换根DP。例如:"对于每个节点,求以该节点为根时,在树上做背包的最优值"

思路是:

  1. 先任选一个根,做树上背包,得到 dp[root]

  2. 用换根DP的思想,将 dp 信息从父节点传递到子节点。

  3. 对每个节点,合并"来自父节点方向"和"来自子树方向"的信息。

3.2 常见题型速查

题型 核心方法 典型例题
树上选点(选/不选) 基础树形DP P1352 没有上司的舞会
树上背包(选课) 树上背包 P2014 选课
每个节点作为根的最优值 换根DP P3478 STA-Station
树上距离最远点 换根DP(维护最长/次长链) P10962 Computer
树上染色 / 边权贡献 树上背包 + 特殊贡献计算 HAOI2015 树上染色

总结

树形DP的进阶之路,可以概括为三个层次:

层次 能力 代表技术
第一层 从叶子向根递推 基础树形DP(选/不选)
第二层 在递推时做选择 树上背包(分组背包合并)
第三层 让每个节点都能成为根 换根DP(两次扫描)

学习树形DP进阶的三个关键点:

  1. 理解树上背包的"分组"本质:每个子树是一组物品,在父节点做分组背包合并。

  2. 掌握换根DP的"两次扫描"套路:第一次从下往上,第二次从上往下。

  3. 学会分析"换根时的变化量":根从父节点转移到子节点时,哪些部分的贡献变了?变化量是多少?