换根法(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 等算法竞赛中,换根法是解决树形问题的必备高级技能之一。

相关推荐
民乐团扒谱机4 小时前
【微实验】倒谱算法(Cepstrum)深度解析:原理、数学推导与代码实现
人工智能·算法·语音识别
Nil2085 小时前
leetcode 189轮转数组
数据结构·算法·leetcode
-dzk-5 小时前
【动态规划】LC 118.杨辉三角
算法·动态规划
恣逍信点5 小时前
《凌微经》静态自悖——变化之自因
人工智能·科技·算法·机器学习·业界资讯·拓扑学·哲学
吃着火锅x唱着歌5 小时前
LeetCode 3885.设计事件管理器
算法·leetcode·职场和发展
土司大王6 小时前
LeetCode hto100——字母异位词分组
java·算法·leetcode
Eloudy6 小时前
预词力:LLM 的唯一形式化能力
人工智能·算法·agent
依然鸣6 小时前
PTA团体程序设计天梯赛L1真题讲解L1-085-088
数据结构·c++·经验分享·算法·深度优先·pat考试
听取WA声一片(无恶意)6 小时前
CSP-J/S 初赛图论完全讲义
算法·csp-s初赛·csp-j初赛
Brilliantwxx7 小时前
【算法从零到千】【51-54】逆序对(分治+递归算法)
数据结构·算法·排序算法