最近公共祖先 (Lowest Common Ancestor / LCA)¶
在有根树中,两个节点 \(u\) 和 \(v\) 的最近公共祖先(LCA)是离根节点最远且同时是 \(u\) 和 \(v\) 的祖先节点。常用于求树上两点距离、树上路径修改与查询等。
树上倍增法 (Binary Lifting)¶
倍增法实现在线查询,预处理时间复杂度 \(\mathcal{O}(n \log n)\),单次查询时间复杂度 \(\mathcal{O}(\log n)\)。
#include <vector>
#include <algorithm>
#include <cmath>
struct TreeLCA {
int n;
int root;
int max_log;
std::vector<int> depth;
std::vector<std::vector<int>> up; // up[k][u] 表示 u 向上走 2^k 步的祖先
TreeLCA(int n, int root, const std::vector<std::vector<int>> &adj)
: n(n), root(root), depth(n + 1, 0) {
max_log = std::__lg(n) + 2;
up.assign(max_log, std::vector<int>(n + 1, 0));
dfs(root, 0, adj);
for (int k = 1; k < max_log; ++k) {
for (int u = 1; u <= n; ++u) {
up[k][u] = up[k - 1][up[k - 1][u]];
}
}
}
void dfs(int u, int p, const std::vector<std::vector<int>> &adj) {
depth[u] = depth[p] + 1;
up[0][u] = p;
for (int v : adj[u]) {
if (v != p) {
dfs(v, u, adj);
}
}
}
// 查询 u 与 v 的 LCA
int query(int u, int v) const {
if (depth[u] < depth[v]) std::swap(u, v);
// 1. 将 u 跳跃到与 v 相同深度
int diff = depth[u] - depth[v];
for (int k = max_log - 1; k >= 0; --k) {
if ((diff >> k) & 1) {
u = up[k][u];
}
}
if (u == v) return u;
// 2. 同时向上倍增跳跃
for (int k = max_log - 1; k >= 0; --k) {
if (up[k][u] != up[k][v]) {
u = up[k][u];
v = up[k][v];
}
}
return up[0][u];
}
// 树上两点之间的边数距离
int dist(int u, int v) const {
return depth[u] + depth[v] - 2 * depth[query(u, v)];
}
};
经典应用¶
- 树上路径点权/边权和:利用树上前缀和 \(S[u]\),两点 \(u, v\) 之间的路径和可表示为 \(S[u] + S[v] - S[\text{lca}] - S[\text{parent}[\text{lca}]]\)。
-
树上差分:
- 点差分:在路径 \(u \to v\) 上所有点权加 \(1\),只需对差分数组操作:
diff[u]++,diff[v]++,diff[lca]--,diff[parent[lca]]--。 - 边差分:在路径 \(u \to v\) 上所有边权加 \(1\),操作为:
diff[u]++,diff[v]++,diff[lca] -= 2。
- 点差分:在路径 \(u \to v\) 上所有点权加 \(1\),只需对差分数组操作:
[!TIP] 进阶演进:如果需要支持树上动态点权/边权的区间修改与区间求和/最值查询,或者需要常数更小的 LCA 算法,推荐使用 树链剖分 (HLD) 配合线段树。通过将树上路径映射为至多 \(\mathcal{O}(\log N)\) 段连续 DFS 序区间,可在 \(\mathcal{O}(\log^2 N)\) 内高效完成动态路径操作。
LeetCode 经典真题精选与题解提示¶
-
LeetCode 236 - 二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree)
中等提示
后序遍历回溯基石。
- 递归函数设计:若当前节点为空或等于 \(p\) 或等于 \(q\),直接返回当前节点;
- 分别递归左右子树获取 \(left\) 和 \(right\)。若左右两子树均返回非空节点,说明 \(p\) 和 \(q\) 分布在当前节点的两侧,当前节点即为 LCA;若仅有一侧非空,返回非空的一侧即可。
-
LeetCode 235 - 二叉搜索树的最近公共祖先 (Lowest Common Ancestor of a Binary Search Tree)
简单提示
BST 有序性分治。
- 从根节点自顶向下走:
- 若 \(p, q\) 的值均小于当前节点,则 LCA 必在当前节点的左子树中;
- 若 \(p, q\) 的值均大于当前节点,则 LCA 必在当前节点的右子树中;
- 首次出现分叉(即当前节点介于 \(p\) 和 \(q\) 之间),当前节点即为分歧点(LCA)。
- 从根节点自顶向下走:
-
LeetCode 1483 - 树节点的第 K 个祖先 (Kth Ancestor of a Tree Node)
困难提示
树上倍增 (Binary Lifting) 模板题。
- 预处理二维数组 \(up[i][u]\) 表示节点 \(u\) 的第 \(2^i\) 代祖先,递推式为 \(up[i][u] = up[i-1][up[i-1][u]]\)。
- 查询第 \(k\) 个祖先时,将 \(k\) 二进制拆分:遍历 \(k\) 的每个二进制位,若第 \(i\) 位为 1 则令 \(u = up[i][u]\),可在 \(\mathcal{O}(\log k)\) 时间内快速跳跃到位。
-
LeetCode 1123 - 最深叶节点的最近公共祖先 (Lowest Common Ancestor of Deepest Leaves)
中等提示
深度与 LCA 联合递归。
- DFS 递归返回二元组
(max_depth, lca_node):- 若左子树深度等于右子树深度,说明两侧均有最深叶节点,当前节点就是最深叶节点的公共祖先;
- 若左子树更深,最深叶节点完全在左子树,LCA 由左子树提供;反之由右子树提供。
- DFS 递归返回二元组
-
LeetCode 2096 - 从二叉树一个节点到另一个节点增加方向 (Step-By-Step Directions From a Binary Tree Node to Another)
中等提示
LCA 路径拆分与拼接。
- 通过 DFS 分别找出从根节点到达起点 \(startValue\) 的路径字符串 \(path_S\)(由
'L'/'R'构成)和到达终点 \(destValue\) 的路径字符串 \(path_T\)。 - 消除两路径的最长公共前缀(相当于向上回退到二者的最近公共祖先 LCA)。
- 起点到 LCA 的剩余长度全部替换为向上走的字符
'U',再拼接上 LCA 到终点的剩余字符即可得到最优路径。
- 通过 DFS 分别找出从根节点到达起点 \(startValue\) 的路径字符串 \(path_S\)(由