欢迎来到起遇信息学
起遇信息学正处于上线筹建阶段,以下功能已全部开放免费体验: ✅ 完整题库浏览与代码提交评测(C / C++ / Python / Java 等) ✅ 入门到进阶的系列课程试读、作业与考试 ✅ AI 提示、AI 作业分析等智能助教功能 ✅ 赛事模拟与个人能力报告 ✅ 邮箱注册开放 ⏳ 付费课程订阅与微信/支付宝支付通道 ⏳ 手机号登录,微信扫码登录、微信公众号绑定 使用中如遇任何问题,欢迎通过页面底部 **"联系我们"** 与我们沟通。
当然。这里的 LCA(Lowest Common Ancestor,最近公共祖先) 是树上非常经典的算法,也是算法竞赛里必须掌握的内容。
我按「是什么 → 怎么想 → 倍增做法 → 代码 → 手算 → 复杂度 → 常见坑」给你讲。
1. LCA 是什么?
给一棵树,任意两个节点 u、v:
LCA(u, v) =
u和v的所有公共祖先中,距离它们最近的那个。
例如:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
比如:
LCA(4, 5) = 2
LCA(4, 8) = 2
LCA(8, 9) = 5
LCA(4, 6) = 1
LCA(8, 3) = 1
2. 为什么需要 LCA?
假设有:
1
├── 2
│ ├── 4
│ └── 5
└── 3
├── 6
└── 7
现在问:
4 到 7 的路径经过哪些点?
实际上:
4 → 2 → 1 → 3 → 7
中间那个关键点就是:
LCA(4,7) = 1
所以 LCA 经常用来解决:
- 树上两点距离
- 树上路径问题
- 树上最近公共节点
- 树上路径修改/查询
- 树上差分
- 倍增
- 树链剖分
3. 最简单的思路
假设:
1
/
2
/
3
/ \
4 5
求:
LCA(4, 5)
我们知道:
4 的祖先:
4 → 3 → 2 → 1
5 的祖先:
5 → 3 → 2 → 1
第一个共同出现的就是:
3
所以:
LCA(4,5)=3
4. 最暴力的方法
我们可以:
- 从
u一直往上走,把所有祖先标记。 - 从
v一直往上走。 - 第一个遇到的标记节点就是 LCA。
例如:
u = 8
8 → 5 → 2 → 1
标记:
8 5 2 1
然后:
v = 9
9 → 5
发现 5 已经被标记:
LCA(8,9)=5
但是最坏情况下:
1
|
2
|
3
|
4
|
...
|
n
一次查询可能需要 O(n)。
如果:
n = 10^5
q = 10^5
就可能达到:
O(nq) = 10^10
肯定不行。
5. LCA 的核心:倍增
这就是竞赛中最经典的 LCA 方法。
核心思想特别简单:
不要让节点一个一个往上跳,而是让它一次跳 1、2、4、8、16……层。
6. fa[u][j] 是什么?
定义:
fa[u][j]
表示:
节点 u 向上跳
2^j层之后到达的节点。
例如:
fa[u][0]
就是:
u 的父亲
因为:
2^0 = 1
fa[u][1]:
向上跳 2 层
fa[u][2]:
向上跳 4 层
fa[u][3]:
向上跳 8 层
所以:
j 跳跃距离
----------------
0 1
1 2
2 4
3 8
4 16
5 32
...
7. 倍增数组怎么计算?
这是 LCA 最重要的公式:
fa[u][j] = fa[ fa[u][j-1] ][j-1];
为什么?
因为:
向上跳 2^j
=
先向上跳 2^(j-1)
再向上跳 2^(j-1)
例如:
2^3 = 8
8 = 4 + 4
所以:
fa[u][3]
就是:
先跳4层
再跳4层
也就是:
fa[ fa[u][2] ][2]
8. DFS 预处理
首先我们需要知道每个节点:
depth[u]
表示深度。
以及:
fa[u][j]
表示第 2^j 个祖先。
例如:
1 depth=0
/ \
2 3 depth=1
/ \
4 5 depth=2
那么:
depth[1] = 0
depth[2] = 1
depth[3] = 1
depth[4] = 2
depth[5] = 2
同时:
fa[4][0] = 2
fa[4][1] = 1
fa[4][2] = 0
9. C++ 标准模板
这是最应该记住的一版:
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
const int LOG = 20;
vector<int> g[N];
int fa[N][LOG];
int depth[N];
void dfs(int u, int father)
{
fa[u][0] = father;
depth[u] = depth[father] + 1;
for (int i = 1; i < LOG; i++)
{
fa[u][i] = fa[fa[u][i - 1]][i - 1];
}
for (int v : g[u])
{
if (v == father)
continue;
dfs(v, u);
}
}
这里最重要的是:
fa[u][0] = father;
以及:
fa[u][i] = fa[fa[u][i - 1]][i - 1];
10. 查询 LCA
现在我们已经有了:
depth[]
fa[][]
那么:
LCA(u,v)
分三步。
第一步:让两个节点处于同一深度
例如:
1
|
2
|
3
|
4
|
5
现在:
u = 5
v = 3
显然:
LCA(5,3)=3
但:
depth[5] = 4
depth[3] = 2
所以我们先把 5 往上跳:
5 → 3
11. 怎么快速跳?
假设:
depth[u] - depth[v] = 13
13 可以拆成:
13 = 8 + 4 + 1
也就是:
2^3 + 2^2 + 2^0
所以:
for (int i = 0; i < LOG; i++)
{
if (diff & (1 << i))
u = fa[u][i];
}
例如:
diff = 13
二进制:
1101
对应:
8 + 4 + 1
所以:
u
↓ 8
↓ 4
↓ 1
总共跳 13 层。
12. 第二步:如果已经相同
假设调整后:
u == v
那么直接:
return u;
为什么?
因为如果一个节点是另一个节点的祖先:
1
|
2
|
3
求:
LCA(2,3)
答案就是:
2
13. 第三步:一起往上跳
这是整个 LCA 最精髓的地方。
假设:
1
/ \
2 3
/ \
4 6
/ \
8 7
求:
LCA(8,7)
现在:
u = 8
v = 7
如果我们直接往上:
8 → 4 → 2 → 1
7 → 6 → 3 → 1
但是我们不知道什么时候该停。
所以采用:
从最大的跳跃开始,只要跳完之后两个人还不是同一个节点,就一起跳。
14. 为什么是“还不是同一个节点”?
假设:
u
\
...
\
LCA
如果:
fa[u][i] != fa[v][i]
说明:
两个人向上跳
2^i层之后,仍然处于 LCA 的两侧。
所以:
u = fa[u][i];
v = fa[v][i];
放心跳。
如果:
fa[u][i] == fa[v][i]
说明:
再跳这么远,就会直接跳到公共祖先甚至越过关键位置。
所以:
不能跳。
最后:
u 和 v
会停在:
LCA 的两个儿子
那么:
return fa[u][0];
就是答案。
15. 完整 LCA 函数
int lca(int u, int v)
{
// 1. 保证 u 比 v 深
if (depth[u] < depth[v])
swap(u, v);
// 2. 让 u、v 深度相同
int diff = depth[u] - depth[v];
for (int i = 0; i < LOG; i++)
{
if (diff & (1 << i))
u = fa[u][i];
}
// 3. 如果 u 已经是 v 的祖先
if (u == v)
return u;
// 4. 从大到小尝试跳
for (int i = LOG - 1; i >= 0; i--)
{
if (fa[u][i] != fa[v][i])
{
u = fa[u][i];
v = fa[v][i];
}
}
// 5. 此时 u、v 是 LCA 的两个儿子
return fa[u][0];
}
这个模板非常重要。
16. 用一个具体例子跑一遍
还是:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \
8 9
求:
LCA(8,7)
第一步:深度
depth[8] = 3
depth[7] = 2
所以:
8 比 7 深 1 层
把 8 往上跳:
8 → 5
现在:
u = 5
v = 7
第二步:从大到小跳
尝试:
2^2 = 4
看:
fa[5][2]
fa[7][2]
显然已经超过树的高度,通常会指向 0,所以不能跳。
再试:
2^1 = 2
假设:
fa[5][1] = 1
fa[7][1] = 1
相同。
不能跳。
再试:
2^0 = 1
:
fa[5][0] = 2
fa[7][0] = 3
不相同。
所以:
5 → 2
7 → 3
现在:
u = 2
v = 3
它们的父亲都是:
1
所以:
return fa[2][0];
得到:
1
因此:
LCA(8,7)=1
17. 为什么时间复杂度是 O(log n)?
假设:
n = 100000
那么:
log₂(100000) ≈ 17
我们只需要保存:
0 ~ 17
这些倍增信息。
也就是:
1
2
4
8
16
32
64
...
65536
一次查询最多遍历这些层数:
O(log n)
18. 完整代码
可以直接用于大部分竞赛题:
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
const int LOG = 20;
vector<int> g[N];
int fa[N][LOG];
int depth[N];
void dfs(int u, int father)
{
fa[u][0] = father;
depth[u] = depth[father] + 1;
for (int i = 1; i < LOG; i++)
{
fa[u][i] = fa[fa[u][i - 1]][i - 1];
}
for (int v : g[u])
{
if (v == father)
continue;
dfs(v, u);
}
}
int lca(int u, int v)
{
// 保证 u 更深
if (depth[u] < depth[v])
swap(u, v);
// 拉到同一深度
int diff = depth[u] - depth[v];
for (int i = 0; i < LOG; i++)
{
if (diff & (1 << i))
u = fa[u][i];
}
// 如果一个是另一个的祖先
if (u == v)
return u;
// 从大到小跳
for (int i = LOG - 1; i >= 0; i--)
{
if (fa[u][i] != fa[v][i])
{
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
// 假设 1 是根
dfs(1, 0);
while (q--)
{
int u, v;
cin >> u >> v;
cout << lca(u, v) << '\n';
}
return 0;
}
19. LCA 最应该记住的 4 个东西
如果你是准备算法竞赛,实际上先把下面四个东西吃透就够了:
① depth
depth[u]
表示节点深度。
② fa[u][0]
fa[u][0] = father;
表示父节点。
③ 倍增公式
fa[u][i] = fa[fa[u][i-1]][i-1];
这是核心。
④ LCA 查询
// 先把深的拉上来
// 再从大到小一起跳
// 最后父亲就是 LCA
可以把它记成一句话:
先齐平,再一起跳;能跳就跳,不能跳就不跳。
20. LCA 和树上距离
LCA 非常常见的一个应用就是:
求树上两个节点之间的距离。
公式:
dist(u,v)
=
depth[u] + depth[v] - 2 × depth[LCA(u,v)]
例如:
1
/ \
2 3
/ \
4 5
求:
4 → 5
有:
depth[4] = 2
depth[5] = 2
LCA(4,5) = 1
所以:
dist(4,5)
= 2 + 2 - 2×1
= 2
路径就是:
4 → 2 → 1 → 3 → 5
等等,这里要注意深度计算:如果根 1 深度为 1,则:
depth[4] = 3
depth[5] = 3
depth[1] = 1
距离:
3 + 3 - 2×1 = 4
正好:
4 → 2 → 1 → 3 → 5
4 条边。
所以竞赛里深度从 0 还是 1 开始都可以,关键是保持一致。
21. 倍增 LCA 的整体思维
你可以把整个算法想象成:
LCA
/ \
/ \
u v
首先:
① 深度不同
↓
把深的拉上来
↓
② 深度相同
↓
从最大步长开始
↓
③ 如果两人跳过去仍不同
↓
一起跳
↓
④ 最终停在 LCA 的两个孩子
↓
⑤ 父节点就是 LCA
核心其实并不复杂。
还有一个非常重要的点
如果你最近是在系统学算法,我建议 LCA 不要只背模板。
下一步最好把这三个东西连起来:
树
↓
DFS
↓
倍增
↓
LCA
↓
树上距离
↓
树上路径问题
尤其是 “为什么 fa[u][i] != fa[v][i] 才能跳” 搞懂之后,LCA 基本就真正掌握了,而不是死记代码。
0 条评论
目前还没有评论...
Be the first to comment!