LCA 学习笔记
概念
LCA
LCA (Lowest Common Ancestor),即最近公共祖先,指的是一棵树中任意两个节点深度最大的共同祖先。
有啥用
树有一个性质,两点之间有且只有一条简单路径,如果我们把 1 号节点作为根,则任意两点 的简单路径就是 到 再到 。
所以这个东西大有用处,于是如何快速的求出两点之间的 就非常重要了。
朴素算法
我们通过一遍深搜处理出所有节点的父节点以及深度(这里的深度指到根节点的路径上的节点个数,与边权无关),然后对于每一次询问,我们先把更深的节点一步一步往上跳到与另一个节点一样的深度,然后再一起一步一步往上跳,直到跳到同一个节点,这个节点就是它们的 LCA。
这样每次询问的时间复杂度是树的深度,在精心构造的数据中可以达到 ,还是比较慢的。
倍增
思想
我们设 表示 号节点的第 级祖先的节点编号(1 级祖先即为父节点,0 级祖先为自己),若不存在,则 。
我们考虑加速刚才朴素算法的过程。
- 把两个节点跳到同一高度
对于两个节点 ,设他们的深度分别为 且 ,则我们需要把 变成 的第 级祖先,设 。我们可以倍增地往上跳,从大到小枚举 2 的幂,若当前 2 的幂 满足 ,就把 更新为 ,然后 减去 即可。这样的时间复杂度是 的,若一棵树深度为 ,则 最坏为 。
- 一起往上跳
还是同样的思想,我们依然是从大到小去枚举 2 的幂,对于 来说,如果 ,我们就把 变为 , 变为 。最后 并不是答案,而是 的两个子节点,所以 (或 )才是答案。这样的时间复杂度也是 的。
所以倍增对于每次询问的时间复杂度是 的,很明显是快了很多。
实现
- 初始化
我们先一遍深搜求出每个结点的层级,父节点,然后对于 ,我们可以这样递推:,时间复杂度为 。
void dfs(int x, int pr, int lv) {
lvl[x] = lv, p[x][0] = pr;
for (int i = 0; i < (int)e[x].size(); i++)
if (e[x][i].to != pr)
dfs(e[x][i].to, x, lv + 1);
}
void initP() {
for (int i = 0; i <= 20; i++)
pw[i] = (1 << i);
for (int j = 1; pw[j] <= n; j++)
for (int i = 1; i <= n; i++)
p[i][j] = p[p[i][j - 1]][j - 1];
}
- 查询第 级祖先
代码很简短,与上面说的一样。
int Qry(int ch, int num) {
int ans = ch;
for (int j = 20; j >= 0; j--)
if (pw[j] <= num)
ans = p[ans][j], num -= pw[j];
return ans;
}
- 查询
代码不长,思想前面已经说过。
int lca(int x, int y) {
if (lvl[x] < lvl[y])
swap(x, y);
x = Qry(x, lvl[x] - lvl[y]);
if (x == y)
return x;
for (int j = 20; j >= 0; j--)
if (p[x][j] != p[y][j])
x = p[x][j], y = p[y][j];
return p[x][0];
}
一些例题
【模板】最近公共祖先(LCA)
题目链接:【模板】最近公共祖先(LCA)
思路:
模板,记得要加输入输出优化(scanf 和 printf)。
代码:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 5e5 + 5;
const int MAX_LOG_N = 30;
struct Edge {
int to, val;
Edge(int _to, int _val) {
to = _to, val = _val;
}
};
vector<Edge> e[MAXN];
int n, p[MAXN][MAX_LOG_N] = {{0}}, lvl[MAXN] = {0};
int pw[MAX_LOG_N] = {0};
void dfs(int x, int pr, int lv) {
lvl[x] = lv, p[x][0] = pr;
for (int i = 0; i < (int)e[x].size(); i++)
if (e[x][i].to != pr)
dfs(e[x][i].to, x, lv + 1);
}
void initP() {
for (int i = 0; i <= 20; i++)
pw[i] = (1 << i);
for (int j = 1; pw[j] <= n; j++)
for (int i = 1; i <= n; i++)
p[i][j] = p[p[i][j - 1]][j - 1];
}
int Qry(int ch, int num) {
int ans = ch;
for (int j = 20; j >= 0; j--)
if (pw[j] <= num)
ans = p[ans][j], num -= pw[j];
return ans;
}
int lca(int x, int y) {
if (lvl[x] < lvl[y])
swap(x, y);
x = Qry(x, lvl[x] - lvl[y]);
if (x == y)
return x;
for (int j = 20; j >= 0; j--)
if (p[x][j] != p[y][j])
x = p[x][j], y = p[y][j];
return p[x][0];
}
bool chk(int x, int y) {
return (lvl[x] >= lvl[y] && Qry(x, lvl[x] - lvl[y]) == y);
}
int m, s;
int main() {
cin >> n >> m >> s;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(Edge(v, 0));
e[v].push_back(Edge(u, 0));
}
dfs(s, 0, 1), initP();
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v;
cout << lca(u, v) << endl;;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现