概念
LCA
LCA (Lowest Common Ancestor),即最近公共祖先,指的是一棵树中任意两个节点深度最大的共同祖先。
有啥用
树有一个性质,两点之间有且只有一条简单路径,如果我们把 1 号节点作为根,则任意两点 \(x,y\) 的简单路径就是 \(x\) 到 \(lca(x,y)\) 再到 \(y\)。
所以这个东西大有用处,于是如何快速的求出两点之间的 \(LCA\) 就非常重要了。
朴素算法
我们通过一遍深搜处理出所有节点的父节点以及深度(这里的深度指到根节点的路径上的节点个数,与边权无关),然后对于每一次询问,我们先把更深的节点一步一步往上跳到与另一个节点一样的深度,然后再一起一步一步往上跳,直到跳到同一个节点,这个节点就是它们的 LCA。
这样每次询问的时间复杂度是树的深度,在精心构造的数据中可以达到 \(O(n)\),还是比较慢的。
倍增
思想
我们设 \(p_{i,j}\) 表示 \(i\) 号节点的第 \(2^j\) 级祖先的节点编号(1 级祖先即为父节点,0 级祖先为自己),若不存在,则 \(p_{i,j}=-1\)。
我们考虑加速刚才朴素算法的过程。
- 把两个节点跳到同一高度
对于两个节点 \(x,y\),设他们的深度分别为 \(d_x,d_y\) 且 \(d_x \le d_y\),则我们需要把 \(y\) 变成 \(y\) 的第 \(d_x-d_y\) 级祖先,设 \(num=d_x-d_y\)。我们可以倍增地往上跳,从大到小枚举 2 的幂,若当前 2 的幂 \(2^j\) 满足 \(2^j\le num\),就把 \(y\) 更新为 \(p_{y,j}\),然后 \(num\) 减去 \(2^j\) 即可。这样的时间复杂度是 \(O(\log num)\) 的,若一棵树深度为 \(n\),则 \(O(\log num)\) 最坏为 \(O(\log n)\)。
- 一起往上跳
还是同样的思想,我们依然是从大到小去枚举 2 的幂,对于 \(2^j\) 来说,如果 \(p_{x,j} \not= p_{y,j}\),我们就把 \(x\) 变为 \(p_{x,j}\),\(y\) 变为 \(p_{y,j}\)。最后 \(x,y\) 并不是答案,而是 \(lca(x,y)\) 的两个子节点,所以 \(p_{x,0}\)(或 \(p_{y,0}\))才是答案。这样的时间复杂度也是 \(O(\log n)\) 的。
所以倍增对于每次询问的时间复杂度是 \(O(\log n)\) 的,很明显是快了很多。
实现
- 初始化
我们先一遍深搜求出每个结点的层级,父节点,然后对于 \(p_{i,j}\),我们可以这样递推:\(p_{i,j}=p_{p_{i,j-1},j-1}\),时间复杂度为 \(O(n \log n)\)。
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];
}
- 查询第 \(num\) 级祖先
代码很简短,与上面说的一样。
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;
}
- 查询 \(lca\)
代码不长,思想前面已经说过。
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;
}
标签:lv,int,笔记,学习,num,LCA,lvl,节点 From: https://www.cnblogs.com/rlc202204/p/17009456.html