GESP 8级
最近公共祖先 LCA
8级 · 高级数据结构与DP
最近公共祖先(Lowest Common Ancestor, LCA)是指在树中,两个节点的最近的共同祖先——就是两个节点"向上走"最先相遇的那个节点。常用倍增法在 O(log n) 时间内求解。
💡 这是什么?
家族树比喻:想象一个家谱。你和你的表哥的"最近公共祖先"是谁?就是你们共同的爷爷(或外公)。再比如,你和你爸爸的最近公共祖先就是你爸爸自己。
正式定义:在有根树中,节点 a 和节点 b 的 LCA 是同时满足以下条件的节点 c:
① c 是 a 的祖先(c 在从 a 到根的路径上)
② c 是 b 的祖先
③ c 的深度尽可能大(即尽可能"低",最靠近 a 和 b)
倍增法核心思路:
① 先用 BFS 预处理出每个节点的深度 depth 和 2^j 级祖先 fa[i][j]
② 求 LCA 时:先让深的节点跳到和浅的节点同一深度,然后两个一起向上跳
正式定义:在有根树中,节点 a 和节点 b 的 LCA 是同时满足以下条件的节点 c:
① c 是 a 的祖先(c 在从 a 到根的路径上)
② c 是 b 的祖先
③ c 的深度尽可能大(即尽可能"低",最靠近 a 和 b)
倍增法核心思路:
① 先用 BFS 预处理出每个节点的深度 depth 和 2^j 级祖先 fa[i][j]
② 求 LCA 时:先让深的节点跳到和浅的节点同一深度,然后两个一起向上跳
🌟 为什么重要?
• 树上距离:a 到 b 的距离 = depth[a] + depth[b] - 2×depth[LCA(a,b)]
• 路径查询:很多树上问题需要找两个节点的路径,而路径必然经过它们的 LCA
• 竞赛高频:树链剖分、虚树等高级技巧都以 LCA 为基础
• 实际应用:版本控制系统(git merge)中,合并两个分支就是找它们的 LCA
• GESP 8级必考:倍增法 LCA 是必须掌握的
• 路径查询:很多树上问题需要找两个节点的路径,而路径必然经过它们的 LCA
• 竞赛高频:树链剖分、虚树等高级技巧都以 LCA 为基础
• 实际应用:版本控制系统(git merge)中,合并两个分支就是找它们的 LCA
• GESP 8级必考:倍增法 LCA 是必须掌握的
📋 前置知识(学这个之前你需要知道)
1. 树的基本概念(GESP 5-6级):有根树、父节点、子节点、深度、祖先
2. BFS 广度优先搜索(GESP 3级):用来预处理 depth 和 fa 数组
3. 二进制分解思想:任何正整数都可以表示为若干个 2 的幂次之和
4. 邻接表存储树:树是无环连通图,用邻接表存储
2. BFS 广度优先搜索(GESP 3级):用来预处理 depth 和 fa 数组
3. 二进制分解思想:任何正整数都可以表示为若干个 2 的幂次之和
4. 邻接表存储树:树是无环连通图,用邻接表存储
📐 倍增法核心公式
fa[i][j] = 节点 i 的第 2^j 级祖先
递推公式:fa[v][j] = fa[ fa[v][j-1] ][j-1]
含义:v 的第 2^j 级祖先 = (v 的第 2^(j-1) 级祖先) 的第 2^(j-1) 级祖先
比如:fa[v][3] = fa[ fa[v][2] ][2]
v 的第8级祖先 = (v的第4级祖先)的第4级祖先
j 最多取到 log₂(n),比如 n=100000 时 j 最大取 17
递推公式:fa[v][j] = fa[ fa[v][j-1] ][j-1]
含义:v 的第 2^j 级祖先 = (v 的第 2^(j-1) 级祖先) 的第 2^(j-1) 级祖先
比如:fa[v][3] = fa[ fa[v][2] ][2]
v 的第8级祖先 = (v的第4级祖先)的第4级祖先
j 最多取到 log₂(n),比如 n=100000 时 j 最大取 17
💻 完整代码(带详细注释)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int N = 100010;
const int LOG = 20; // log₂(100000) ≈ 17,开到20够用
vector<int> adj[N]; // 邻接表存树
int depth[N]; // depth[i] = 节点 i 的深度(根为1)
int fa[N][LOG]; // fa[i][j] = 节点 i 的第 2^j 级祖先
// BFS 预处理:计算 depth 和 fa 数组
void bfs_lca(int root) {
queue<int> q;
q.push(root);
depth[root] = 1; // 根节点深度设为1(方便判断,depth=0表示未访问)
while (!q.empty()) {
int u = q.front();
q.pop();
// 遍历 u 的所有邻居(子节点)
for (int v : adj[u]) {
if (depth[v]) continue; // 已访问过,跳过(v是u的父节点)
depth[v] = depth[u] + 1; // 子节点深度 = 父节点深度 + 1
fa[v][0] = u; // v 的第 1 级祖先(2^0=1)就是 u
// 倍增:预处理 v 的所有 2^j 级祖先
for (int j = 1; j < LOG; j++) {
// v 的第 2^j 级祖先 = (v的第 2^(j-1) 级祖先)的第 2^(j-1) 级祖先
fa[v][j] = fa[ fa[v][j-1] ][j-1];
}
q.push(v);
}
}
}
// 求节点 a 和 b 的最近公共祖先
int lca(int a, int b) {
// 第1步:确保 a 是较深的节点(如果 a 比 b 浅就交换)
if (depth[a] < depth[b]) swap(a, b);
// 第2步:把 a 向上跳,跳到和 b 同一深度
// 从大到小枚举 j,利用二进制分解
for (int j = LOG - 1; j >= 0; j--) {
// 如果 a 跳 2^j 步后仍然比 b 深(或同样深),就跳
if (depth[fa[a][j]] >= depth[b])
a = fa[a][j]; // a 向上跳 2^j 步
}
// 特殊情况:如果跳到同一深度后 a==b,说明 b 就是 a 的祖先
if (a == b) return a;
// 第3步:a 和 b 同一深度但不是同一个点
// 一起向上跳,跳到 LCA 的下一层
for (int j = LOG - 1; j >= 0; j--) {
if (fa[a][j] != fa[b][j]) {
// 如果跳 2^j 步后两个节点不同,说明 LCA 还在上面
a = fa[a][j]; // a 向上跳
b = fa[b][j]; // b 向上跳
}
}
// 此时 a 和 b 的父节点就是 LCA
return fa[a][0];
}
int main() {
int n, q; // n 个节点,q 次查询
cin >> n >> q;
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v); // 无向树,双向加边
adj[v].push_back(u);
}
bfs_lca(1); // 以节点1为根,预处理
while (q--) {
int a, b;
cin >> a >> b;
cout << lca(a, b) << endl;
}
return 0;
}
🔍 倍增法执行过程举例
树:
1 (depth=1)
/ \
2 3 (depth=2)
/
4 (depth=3)
预处理 fa 数组:
fa[2][0]=1, fa[2][1]=fa[fa[2][0]][0]=fa[1][0]=0
fa[3][0]=1, fa[3][1]=fa[fa[3][0]][0]=fa[1][0]=0
fa[4][0]=2, fa[4][1]=fa[fa[4][0]][0]=fa[2][0]=1, fa[4][2]=fa[fa[4][1]][1]=fa[1][1]=0
求 LCA(4, 3):
depth[4]=3 > depth[3]=2,a=4, b=3
Step 2:让 a 跳到和 b 同深度
j=1: depth[fa[4][1]]=depth[1]=1 < depth[b]=2 → 不跳
j=0: depth[fa[4][0]]=depth[2]=2 ≥ depth[b]=2 → a=fa[4][0]=2
现在 a=2, b=3,a≠b
Step 3:一起向上跳
j=1: fa[2][1]=0 == fa[3][1]=0 → 不跳
j=0: fa[2][0]=1 ≠ fa[3][0]=1? → 实际都是1,不跳
return fa[a][0] = fa[2][0] = 1
LCA(4,3) = 1 ✓
1 (depth=1)
/ \
2 3 (depth=2)
/
4 (depth=3)
预处理 fa 数组:
fa[2][0]=1, fa[2][1]=fa[fa[2][0]][0]=fa[1][0]=0
fa[3][0]=1, fa[3][1]=fa[fa[3][0]][0]=fa[1][0]=0
fa[4][0]=2, fa[4][1]=fa[fa[4][0]][0]=fa[2][0]=1, fa[4][2]=fa[fa[4][1]][1]=fa[1][1]=0
求 LCA(4, 3):
depth[4]=3 > depth[3]=2,a=4, b=3
Step 2:让 a 跳到和 b 同深度
j=1: depth[fa[4][1]]=depth[1]=1 < depth[b]=2 → 不跳
j=0: depth[fa[4][0]]=depth[2]=2 ≥ depth[b]=2 → a=fa[4][0]=2
现在 a=2, b=3,a≠b
Step 3:一起向上跳
j=1: fa[2][1]=0 == fa[3][1]=0 → 不跳
j=0: fa[2][0]=1 ≠ fa[3][0]=1? → 实际都是1,不跳
return fa[a][0] = fa[2][0] = 1
LCA(4,3) = 1 ✓
⏱ 复杂度分析
预处理(BFS + 倍增):O(n × log n)
每次查询:O(log n)
总复杂度:O((n + q) × log n),q 是查询次数
空间:O(n × log n),fa 数组大小
比暴力法 O(n) 每次查询快得多!
每次查询:O(log n)
总复杂度:O((n + q) × log n),q 是查询次数
空间:O(n × log n),fa 数组大小
比暴力法 O(n) 每次查询快得多!
📐 常见衍生问题
树上两点距离:
dist(a, b) = depth[a] + depth[b] - 2 × depth[LCA(a,b)]
为什么这个公式成立?
a→LCA 的距离 = depth[a] - depth[LCA]
b→LCA 的距离 = depth[b] - depth[LCA]
两者相加 = depth[a] + depth[b] - 2×depth[LCA]
dist(a, b) = depth[a] + depth[b] - 2 × depth[LCA(a,b)]
为什么这个公式成立?
a→LCA 的距离 = depth[a] - depth[LCA]
b→LCA 的距离 = depth[b] - depth[LCA]
两者相加 = depth[a] + depth[b] - 2×depth[LCA]
⚠️ 易错点
1. depth[0]=0 这个细节 → 未访问的节点 depth=0,根的 depth≥1。fa[根][j] 会指向 0,depth[0]=0 恰好能让条件 depth[fa[a][j]] >= depth[b] 正确工作。
2. fa 数组初始化 → 全局变量默认为0,所以 fa[根][j]=0 是正确的。不要手动初始化为 -1。
3. 交换后别搞混 → swap(a,b) 后,a 一定是较深的那个。
4. j 要从大到小枚举 → 从 LOG-1 到 0,先跳大步再跳小步,类似二进制分解。
5. 最后 return fa[a][0] → 不是 return a!跳完循环后 a 和 b 在 LCA 的下一层,它们的父节点才是 LCA。
6. 无向树要加双向边 → adj[u].push_back(v) 和 adj[v].push_back(u) 都要写。
2. fa 数组初始化 → 全局变量默认为0,所以 fa[根][j]=0 是正确的。不要手动初始化为 -1。
3. 交换后别搞混 → swap(a,b) 后,a 一定是较深的那个。
4. j 要从大到小枚举 → 从 LOG-1 到 0,先跳大步再跳小步,类似二进制分解。
5. 最后 return fa[a][0] → 不是 return a!跳完循环后 a 和 b 在 LCA 的下一层,它们的父节点才是 LCA。
6. 无向树要加双向边 → adj[u].push_back(v) 和 adj[v].push_back(u) 都要写。
🎯 练习建议
入门练习:
• 洛谷 P3379 【模板】最近公共祖先(LCA)(直接套模板)
进阶练习:
• 洛谷 P1395 会议(LCA + 树上距离)
• 洛谷 P4419 [NOI2018]你的名字(LCA应用)
• 洛谷 P2633 统计医生治疗快乐度(主席树 + LCA,非常难)
学习方法:
① 先手写 BFS 预处理 depth 和 fa[ ][0]
② 理解倍增公式 fa[v][j] = fa[ fa[v][j-1] ][j-1]
③ 分步理解 lca 函数:先调深度,再一起跳
④ 背下模板,考试时直接套用
• 洛谷 P3379 【模板】最近公共祖先(LCA)(直接套模板)
进阶练习:
• 洛谷 P1395 会议(LCA + 树上距离)
• 洛谷 P4419 [NOI2018]你的名字(LCA应用)
• 洛谷 P2633 统计医生治疗快乐度(主席树 + LCA,非常难)
学习方法:
① 先手写 BFS 预处理 depth 和 fa[ ][0]
② 理解倍增公式 fa[v][j] = fa[ fa[v][j-1] ][j-1]
③ 分步理解 lca 函数:先调深度,再一起跳
④ 背下模板,考试时直接套用