换根法(Rerooting)算法详解

1. 什么是换根法

换根法(Rerooting)是一种在树形数据结构(通常是树或图)上解决动态规划问题的算法技巧。其核心思想是:先以树中任意一个节点为根,进行一次深度优先搜索(DFS),计算出以该节点为根的子树相关的答案;然后通过第二次 DFS,利用已计算出的信息,高效地将根节点"换"到其他节点,并计算出以新节点为根时的答案。

这种方法避免了为每个节点作为根都进行一次完整的 O(n) 遍历,从而将总时间复杂度从 O(n²) 优化到 O(n)。

2. 算法思想与步骤

2.1 第一次 DFS:预处理(自底向上)

任选一个节点(通常为节点 0 或 1)作为初始根,进行一次后序遍历(Post-order Traversal)。

  • 计算每个节点只考虑其子树时的状态值。例如:子树大小、子树节点权值和、子树中最长路径等。
  • 将子节点的信息"贡献"给父节点,完成自底向上的信息聚合。

这一步结束后,我们得到了以初始根节点为根时,所有节点的"子树信息"。

2.2 第二次 DFS:换根(自顶向下)

从初始根节点开始,进行前序遍历(Pre-order Traversal)。

  • 当从父节点 u 遍历到子节点 v 时,我们已知以 u 为根时整棵树的信息。
  • 现在要将根从 u "换"到 v。这意味着:
    • 节点 v 的子树(相对于原根)保持不变。
    • 节点 u 及其除 v 以外的其他子树,将变成节点 v 的一棵新子树。
  • 利用第一次 DFS 计算出的"子树信息",我们可以 O(1) 或 O(子节点数) 地计算出以 v 为根时所需的信息。
  • 将计算出的以 v 为根的答案记录下来,然后递归地对 v 的子节点进行同样的换根操作。

3. 经典应用场景

  • 求树中每个节点到其他所有节点的距离之和:LeetCode 310. Minimum Height Trees 的变体,或直接求"距离和"。
  • 求树中以每个节点为根时的子树大小/权值和
  • 求树中以每个节点为根时的最长路径(树的直径)
  • 求树中以每个节点为根时,满足某种条件(如颜色、奇偶性)的节点个数
  • 在加权树上求每个节点到其他节点的最大/最小代价

4. 代码模板(C++)

cpp 复制代码
#include <vector>
#include <functional>
using namespace std;
/**
换根法通用模板框架(C++)
假设问题:求以每个节点为根时,其子树节点值的和(节点值默认为1,即求子树大小)。
*/
vector<int> rerooting(int n, vector<vector<int>>& edges) {
// 建图
vector<vector<int>> graph(n);
for (auto& e : edges) {
int u = e[0], v = e[1];
graph[u].push_back(v);
graph[v].push_back(u);
}
// 第一次DFS:后序遍历,计算子树信息 dp_sub
vector<int> dp_sub(n, 0); // dp_sub[u] 表示以u为根的子树节点和
function<void(int, int)> dfs1 = [&](int u, int parent) {
dp_sub[u] = 1; // 节点自身
for (int v : graph[u]) {
if (v == parent) continue;
dfs1(v, u);
dp_sub[u] += dp_sub[v]; // 累加子树的贡献
}
};
dfs1(0, -1); // 假设以0为初始根
// 第二次DFS:前序遍历,进行换根,计算最终答案 ans
vector<int> ans(n, 0); // ans[u] 表示以u为根时整棵树的节点和
ans[0] = dp_sub[0]; // 初始根的结果就是其子树和(即整棵树)
function<void(int, int)> dfs2 = [&](int u, int parent) {
for (int v : graph[u]) {
if (v == parent) continue;
// 将根从 u 换到 v
// 1. 从 u 的贡献中移除 v 的子树贡献
// 2. 将 u(及其剩余部分)作为 v 的新子树贡献加给 v
// 对于"子树节点和"问题:
//   ans[v] = dp_sub[v] + (ans[u] - dp_sub[v])
// 化简后:ans[v] = ans[u]
// 但通用情况需要更复杂的转移,这里展示框架
ans[v] = dp_sub[v] + (ans[u] - dp_sub[v]);
dfs2(v, u);
}
};
dfs2(0, -1);
return ans;
}
// 示例:一棵4个节点的链 0-1-2-3
int main() {
int n = 4;
vector<vector<int>> edges = {{0,1},{1,2},{2,3}};
vector<int> result = rerooting(n, edges);
// 输出每个节点为根时的子树节点和(实际上对于链,就是节点数4)
for (int val : result) {
printf("%d ", val);
}
return 0;
}

5. 关键点与注意事项

  • 状态定义:明确第一次 DFS 要计算的"子树信息"是什么,以及如何从子节点转移到父节点。
  • 换根转移方程 :这是最核心的部分。需要推导出当根从父节点 u 换到子节点 v 时,如何利用已知的 dp_subans[u] 快速计算出 ans[v]
  • 初始化:注意初始根节点答案的初始化。
  • 避免重复计算 :在第二次 DFS 中,当计算子节点 v 的答案时,要确保使用的父节点 u 的答案是最新的(即已经完成了换根到 u 的计算)。
  • 复杂度:两次 DFS 都是 O(n),总时间复杂度 O(n),空间复杂度 O(n)。

6. 实战例题解析(LeetCode 310. Minimum Height Trees)

问题可以转化为:对于无向图(树),求以每个节点为根时的树高,然后找出最小高度对应的根节点集合。

使用换根法:

  1. 第一次 DFS 计算每个节点向下的最大高度和次大高度(用于换根时,当最深的路径经过当前子节点时,需要使用次大高度)。
  2. 第二次 DFS 进行换根,计算每个节点向上的高度(即从父节点方向来的最长路径),然后结合向下的高度,得到以该节点为根时的总高度。

(此处可展开详细代码,限于篇幅省略)

7. 总结

换根法是一种非常高效的树形 DP 优化技巧,将"为每个根计算答案"的问题从 O(n²) 优化到 O(n)。掌握其核心的两次 DFS 流程(自底向上预处理 + 自顶向下换根)以及状态转移方程的推导,是解决此类问题的关键。在 LeetCode、Codeforces 等算法竞赛中,换根法是解决树形问题的必备高级技能之一。

相关推荐
晓子文集1 小时前
Tushare接口文档:期货交易日历(fut_trade_cal)
大数据·算法
^yi2 小时前
【Linux系统编程】进程状态的理解
算法·僵尸进程·孤儿进程·进程状态·挂起状态·阻塞状态
冻柠檬飞冰走茶3 小时前
PTA基础编程题目集 7-7 12-24小时制(C语言实现)
c语言·开发语言·数据结构·算法
长不胖的路人甲3 小时前
二叉排序树(BST)Java 完整实现 + 删除思路详解
java·开发语言·算法
geovindu3 小时前
python: Breadth First Search Algorithm and Depth First Search Algorithm
开发语言·后端·python·算法·搜索算法
长不胖的路人甲3 小时前
可达性分析法(根搜索算法)完整详解
java·jvm·算法
paeamecium4 小时前
【PAT甲级真题】- Kuchiguse (20)
数据结构·c++·python·算法·pat考试·pat
青山木4 小时前
Hot 100 --- 全排列
java·数据结构·算法·leetcode·深度优先
海兰4 小时前
【高速缓存】RedisVL 存储类型选择指南:Hash 与 JSON
人工智能·redis·算法·缓存·json·哈希算法