一、什么是最近公共祖先(LCA)
1.1 基本定义
在一棵有根树中,给定两个节点 u 和 v,它们的最近公共祖先(LCA)是指:
- 同时是
u和v的祖先的节点中,深度最大的那个。 - 也就是说,它是离
u和v最近的共同祖先。
📌 祖先:从根节点到该节点路径上的所有节点(包括父节点、祖父节点等)都称为它的祖先。
1.2 举例说明
1
/ \
2 3
/ \
4 5
/
6
- LCA(4, 5) = 2
- LCA(6, 5) = 2
- LCA(6, 3) = 1
- LCA(4, 4) = 4
二、LCA 的应用场景
-
求树上两点之间的路径长度
距离公式:dist(u, v) = dep[u] + dep[v] - 2 * dep[LCA(u, v)] -
网络路由中的共同源点查找
-
生物信息学中系统发育树分析
-
在线查询树中任意两点的路径信息
三、LCA 的常见解法
| 方法 | 预处理时间 | 查询时间 | 空间复杂度 | 是否支持在线 |
|---|---|---|---|---|
| 暴力法 | O(1) | O(n) | O(n) | 是 |
| 倍增法(Binary Lifting) | O(n log n) | O(log n) | O(n log n) | ✅ 是 |
| Tarjan(离线) | O(n + m) | O(α(n)) | O(n) | ❌ 否 |
| 树链剖分 | O(n) | O(log n) | O(n) | ✅ 是 |
| RMQ(欧拉序+ST表) | O(n log n) | O(1) | O(n log n) | ✅ 是 |
本文重点讲解最常用、易理解、适合竞赛的 倍增法(Binary Lifting)
四、倍增法详解(Binary Lifting)
4.1 核心思想
利用二进制跳跃的思想,对每个节点预处理出其向上跳 2^0, 2^1, 2^2, …, 2^k 步的祖先节点。
这样在查询时就可以像“快速幂”一样,快速将两个节点提到同一高度,并逐步逼近 LCA。
4.2 关键数组定义
dep[u]:节点u的深度(根节点深度为 1 或 0,本文设为 1)fa[u][i]:节点u向上跳2^i步所到达的节点(即第2^i级祖先)
例如:
fa[u][0]是u的父节点(跳 1 步)fa[u][1]是u的祖父节点(跳 2 步)fa[u][2]是u的曾祖父节点(跳 4 步)
4.3 动态规划转移方程
fa[u][i] = fa[ fa[u][i-1] ][i-1]
解释:从 u 跳 2^i 步 = 先跳 2^(i-1) 步到中间点,再从中间点跳 2^(i-1) 步。
4.4 查询步骤
给定两个节点 u 和 v:
-
调整深度一致
假设dep[u] > dep[v],让u向上跳,直到与v深度相同。 -
同步向上跳跃
从最大的步长开始(如i = 20到0),如果fa[u][i] != fa[v][i],说明跳2^i步还没到 LCA,可以跳。否则不跳。 -
返回结果
跳完后,u和v的父节点就是 LCA。
✅ 为什么最后返回
fa[u][0]?因为此时u和v都在 LCA 的下一层。
五、C++ 完整实现(倍增法)
#include <bits/stdc++.h>
using namespace std;
// ============ 常量定义 ============
const int MAXN = 5e5 + 5; // 最大节点数
const int LOG = 20; // 2^20 > 1e6,足够覆盖深度
// ============ 全局变量 ============
int n, m, s; // 节点数、查询数、根节点
vector<int> tree[MAXN]; // 邻接表存储树
int dep[MAXN]; // 每个节点的深度
int fa[MAXN][LOG]; // fa[u][i] 表示 u 的第 2^i 级祖先
// ============ DFS 预处理深度和一级祖先 ============
void dfs(int u, int parent) {
dep[u] = dep[parent] + 1; // 根节点的 parent 是 0,dep[0] = 0
fa[u][0] = parent;
// 预处理 2^1 到 2^(LOG-1) 级祖先
for (int i = 1; i < LOG; ++i) {
fa[u][i] = fa[fa[u][i-1]][i-1];
// 如果已经跳到根以上,fa[u][i] 会是 0,不影响
}
// 遍历子节点
for (int v : tree[u]) {
if (v == parent) continue;
dfs(v, u);
}
}
// ============ LCA 查询函数 ============
int lca(int u, int v) {
// 保证 u 的深度 >= v 的深度
if (dep[u] < dep[v]) swap(u, v);
// Step 1: 将 u 提升到与 v 相同深度
int diff = dep[u] - dep[v];
for (int i = 0; i < LOG; ++i) {
if (diff & (1 << i)) { // 如果第 i 位是 1,就跳 2^i 步
u = fa[u][i];
}
}
// 如果此时 u == v,说明 v 就是 LCA
if (u == v) return u;
// Step 2: 同时向上跳,寻找 LCA
for (int i = LOG - 1; i >= 0; --i) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
// 此时 u 和 v 的父节点就是 LCA
return fa[u][0];
}
// ============ 主函数 ============
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
// 输入节点数、查询数、根节点
cin >> n >> m >> s;
// 构建无向树
for (int i = 1; i <= n - 1; ++i) {
int u, v;
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
// 从根节点开始 DFS 预处理
dfs(s, 0);
// 处理 m 个查询
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
cout << lca(u, v) << '\n';
}
return 0;
}
六、代码详细解析
6.1 输入格式
n m s
u1 v1
u2 v2
...
um-1 vm-1
q1 q2
q3 q4
...
qm
n:节点数m:查询次数s:根节点编号- 接下来
n-1行是边 - 最后
m行是查询对
6.2 DFS 的作用
- 计算每个节点的深度
dep[] - 初始化
fa[u][0](父节点) - 利用 DP 递推计算
fa[u][i](i ≥ 1)
6.3 LCA 查询逻辑
if (dep[u] < dep[v]) swap(u, v);
确保 u 是更深的那个,方便统一处理。
for (int i = 0; i < LOG; ++i) {
if (diff & (1 << i)) {
u = fa[u][i];
}
}
将 u 向上跳 diff 步,使其与 v 同深度。利用二进制拆分,每次跳 2^i 步。
if (u == v) return u;
说明 v 是 u 的祖先,直接返回。
for (int i = LOG - 1; i >= 0; --i) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
从大步长开始尝试跳跃。只要 fa[u][i] != fa[v][i],说明还没到 LCA,可以跳;否则跳过。
return fa[u][0];
最终 u 和 v 都在 LCA 的正下方,其父节点即为答案。
七、复杂度分析
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 预处理(DFS) | O(n log n) | 每个节点处理 log n 次 |
| 单次查询 | O(log n) | 最多跳 log n 次 |
| 空间复杂度 | O(n log n) | fa[MAXN][LOG] 数组 |
对于
n = 5e5,log n ≈ 19,完全可接受。
八、测试样例
输入:
5 3 1
1 2
1 3
2 4
2 5
4 5
2 3
1 4
输出:
2
1
1
九、注意事项
- 树是无向图,建图时要双向加边。
- 根节点的父节点设为 0,
dep[0] = 0。 LOG的值根据n确定,一般取20足够(支持n ≤ 1e6)。- 使用
ios::sync_with_stdio(false)加速输入输出。
十、拓展:其他 LCA 方法简介
1. Tarjan 算法(离线)
- 基于并查集 + DFS 回溯
- 时间复杂度:O(n + m α(n))
- 缺点:必须一次性知道所有查询,不能在线
2. 树链剖分
- 将树剖分为若干条链
- 每次跳链头,O(log n) 查询
- 代码较长,但常数小,适合重链明显的树
3. RMQ + 欧拉序
- DFS 一遍生成欧拉序和深度序列
- 用 ST 表预处理区间最小值
- 查询 O(1),预处理 O(n log n)
LCA算法详解与实现
5万+

被折叠的 条评论
为什么被折叠?



