第60课 - 树形 DP
学习目标
- 理解树形DP的基本思想:掌握如何将动态规划思想应用到树形结构上
- 掌握树形DP的实现模式:学会使用递归(DFS)进行状态转移
- 理解子树DP与根节点DP的区别:能够处理从子树到根的DP问题
- 学习换根DP(Rerooting)技巧:解决无根树问题的优化方法
- 应用树形DP解决经典问题:如树的直径、节点间最大路径和等
核心概念
什么是树形DP?
树形DP是在树结构上进行的动态规划,通常使用深度优先搜索(DFS)来遍历树并计算状态。它的核心思想是:通过递归计算每个子树的信息,然后合并这些信息来得到整棵树的解。
树形DP的基本模式
- 自底向上:从叶子节点开始,逐步向根节点传递信息
- 状态定义:通常以当前节点为根的子树作为状态
- 状态转移:利用子节点的状态来更新当前节点的状态
换根DP(Rerooting)
换根DP是一种优化技巧,用于解决无根树问题。基本思想是:
- 首先任选一个根节点进行一次树形DP
- 然后通过"换根"操作,将每个节点依次作为根节点,快速计算出所有节点的答案
代码示例
1. 基础树形DP示例:计算树的直径
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
vector<int> tree[MAXN];
int dp[MAXN]; // dp[i]表示以i为根的子树中,从i出发的最长路径长度
int ans = 0; // 记录树的直径
void dfs(int node, int parent) {
dp[node] = 0;
int max1 = 0, max2 = 0; // 记录最长和次长的子路径
for (int child : tree[node]) {
if (child == parent) continue;
dfs(child, node);
// 更新最长和次长路径
if (dp[child] + 1 > max1) {
max2 = max1;
max1 = dp[child] + 1;
} else if (dp[child] + 1 > max2) {
max2 = dp[child] + 1;
}
}
// 当前节点的最长路径是经过当前节点的最长路径
dp[node] = max1;
// 更新全局直径:经过当前节点的最长路径 = max1 + max2
ans = max(ans, max1 + max2);
}
int main() {
int n;
cin >> n;
// 建树
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
dfs(1, -1); // 从节点1开始DFS,-1表示没有父节点
cout << "树的直径长度为: " << ans << endl;
return 0;
}
2. 换根DP示例:计算每个节点到其他节点的距离和
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 100005;
vector<int> tree[MAXN];
int subtree_size[MAXN]; // 子树大小
long long dist_sum[MAXN]; // 以当前节点为根时,到所有节点的距离和
long long ans[MAXN]; // 最终答案
// 第一次DFS:计算子树大小和以1为根时的距离和
void dfs1(int node, int parent) {
subtree_size[node] = 1;
dist_sum[node] = 0;
for (int child : tree[node]) {
if (child == parent) continue;
dfs1(child, node);
subtree_size[node] += subtree_size[child];
// 子树中每个节点到node的距离比到child的距离多1
dist_sum[node] += dist_sum[child] + subtree_size[child];
}
}
// 第二次DFS:换根DP
void dfs2(int node, int parent) {
ans[node] = dist_sum[node];
for (int child : tree[node]) {
if (child == parent) continue;
// 保存原始值用于回溯
long long original_dist_sum = dist_sum[node];
int original_subtree_size = subtree_size[node];
// 将根从node换到child
// 1. node的子树大小减少
subtree_size[node] -= subtree_size[child];
// 2. node的距离和减少(child子树中的节点现在都近了1)
dist_sum[node] -= (dist_sum[child] + subtree_size[child]);
// 3. child成为新的根
subtree_size[child] += subtree_size[node];
dist_sum[child] += (dist_sum[node] + subtree_size[node]);
// 递归处理child
dfs2(child, node);
// 回溯:恢复原始值
dist_sum[node] = original_dist_sum;
subtree_size[node] = original_subtree_size;
// child的值在下次循环会重新计算,不需要回溯
}
}
int main() {
int n;
cin >> n;
// 建树
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
// 第一次DFS
dfs1(1, -1);
// 第二次DFS(换根)
dfs2(1, -1);
// 输出每个节点的距离和
for (int i = 1; i <= n; i++) {
cout << "节点 " << i << " 到其他所有节点的距离和: " << ans[i] << endl;
}
return 0;
}
实践练习
练习1:基础树形DP - 计算树的最长路径(简单)
题目描述:给定一棵n个节点的树,边有权重。请计算树中最长路径的长度(即树中两点间的最长距离)。
输入格式:
n
u1 v1 w1
u2 v2 w2
...
其中w是边的权重。
预期输出:
最长路径长度为: X
示例输入:
5
1 2 3
2 3 4
2 4 2
4 5 1
示例输出:
最长路径长度为: 8
提示:可以使用两次DFS的方法,或者直接使用树形DP。
练习2:中等难度 - 计算节点间路径的最大和(中等)
题目描述:给定一棵n个节点的树,每个节点有一个值。请找到一条路径,使得路径上节点值的和最大。
输入格式:
n
val1 val2 ... valn
u1 v1
u2 v2
...
预期输出:
最大路径和为: X
示例输入:
4
5 6 2 3
1 2
2 3
2 4
示例输出:
最大路径和为: 14
(路径:节点1-2-3,和为5+6+2=13,或节点1-2-4,和为5+6+3=14)
练习3:高级难度 - 换根DP应用(困难)
题目描述:给定一棵n个节点的树,每个节点有一个权值。请为每个节点计算:以该节点为根时,所有子树权值和的最大值。
输入格式:
n
val1 val2 ... valn
u1 v1
u2 v2
...
预期输出:n行,每行一个整数,表示以对应节点为根时,所有子树权值和的最大值。
提示:需要使用换根DP,首先计算以某个节点为根时的所有子树权值和,然后通过换根更新其他节点的答案。
常见错误
1. 忘记处理父节点
// 错误:没有检查父节点,导致重复访问
for (int child : tree[node]) {
// 缺少:if (child == parent) continue;
dfs(child, node); // 可能又回到父节点,导致无限递归
}
2. 换根DP中状态转移错误
// 错误:没有正确计算新根的状态
void dfs2(int node, int parent) {
for (int child : tree[node]) {
if (child == parent) continue;
// 错误示例:直接修改没有考虑子树大小的变化
dist_sum[child] = dist_sum[node] - subtree_size[child] + (n - subtree_size[child]);
// 正确应该是:dist_sum[child] = dist_sum[node] - subtree_size[child] + (n - subtree_size[child]);
}
}
3. 数组越界
// 错误:没有考虑节点编号从1开始还是从0开始
int dp[MAXN];
// 如果节点编号从1开始,应该确保MAXN足够大
4. 遗漏初始化
// 错误:没有初始化DP数组
void dfs(int node, int parent) {
// dp[node] 可能包含上次计算的残留值
// 应该先初始化:dp[node] = 0;
}
5. 全局变量污染
// 错误:多次调用DFS时没有重置全局变量
int ans = 0; // 全局变量,在多次测试案例中没有重置
小结
-
树形DP特点:在树结构上使用动态规划,通常自底向上计算,状态定义为以当前节点为根的子树信息。
-
实现模式:使用DFS递归遍历树,先处理子节点,再用子节点信息更新当前节点状态。
-
换根DP技巧:解决无根树问题的有效方法,通过两次DFS(第一次计算基础状态,第二次换根更新)实现O(n)时间复杂度。
-
常见应用:
- 树的直径问题
- 节点间最大路径和
- 以每个节点为根的某种最优值
- 树的重心、中心等
-
注意事项:
- 一定要处理父节点,避免循环递归
- 注意状态定义的正确性
- 换根DP要注意状态转移的正确计算
- 注意数组大小和初始化
关键思想:树形DP将复杂问题分解为子树问题,利用树的递归性质进行高效求解。掌握换根DP技巧可以解决很多看似需要O(n²)复杂度的无根树问题。
练习编辑器
rust
Loading...