什么是LCA?
LCA就是最近公共祖先的缩写,就是假如我们有下面的一个树。那么这个树上的10号结点与7号结点的LCA就是2号结点
暴力的思路
在讲正解之前我们先来讲讲如何用暴力来解决这个问题。因为倍增就是对暴力的改进。加入我们要求7号结点与10号结点的LCA,那么我们可以先从深度较大的结点移动到与深度较浅的结点的深度相同的地方。比如说这里深度较大的结点是10号结点,然后我们要从10号结点跳到6号结点,然后现在这两个结点的深度一样了。之后我们从6号结点与7号结点同时往上跳,然后我们总会有一个时候跳到同一个位置。这个位置即使2号结点然后我们就求出了7号结点与10号结点的LCA 2 号结点
倍增的思路:
假如你天资聪慧,骨骼精奇 你一定会发现这个方法假如在树的深度不是很大的时候还算管用,但是假如这个树的长的类似一个链,那么上面的方法就会很慢了。
那么我们要怎么办呢?我们考虑倍增。我们有一个数组f[i][j]
来表示一个第i
号点跳 $ 2^j $ 步所到达的结点。然后我们可以得到一个递推式:
f[i][j] = f[f[i][j - 1]][j - 1];
原因就是假如一个结点从i
号结点往上跳 \(2^{j -1}\) 再跳 $ 2^{j - 1} $ 相当于从 i
号结点跳 $ 2^{j - 1} + 2 ^ {j - 1} = 2 \times 2 ^ {j - 1} = 2^j$ 步。然后这个地推式的边界就是
f[i][0] = fa[i]; //fa[i]表示结点i的父亲
然后我们还需要一个depth[i]
数组,这个数组存的是每个点的深度(一个点的深度就是这个点到这个树的根所在的点的距离,如果是一个无根树,那么就是到dfs起点的距离)。上面的f[i][j]
数组与 depth[i]
数组都可以用 dfs
求出。代码如下:
void dfs(int x, int dep) {
for (register int i = 1; i < 25; ++i)
f[x][i] = f[f[x][i - 1]][i - 1];
for (register int i = head[x]; i != 0 && Edge[i].s == x; i = Edge[i].nxt) {
if (vis[Edge[i].e]) continue;
vis[Edge[i].e] = true;
f[Edge[i].e][0] = x;
depth[Edge[i].e] = dep + 1;
dfs(Edge[i].e, dep + 1);
}
}
Ps:我是这样存边的
struct EDGE {
int s; //这个边的起点
int e; //这个边的终点
int w; //这个边的权值
int nxt; //这个边的下一条边的指针
} Edge[N * 2];
之后又我们就可以开始求LCA了。
首先我们还是与暴力的思路一样,先将从深度较大的点走到与深度较小的点的深度一样的点。代码如下:
if (depth[a] < depth[b]) swap(a, b); //保证a结点的深度最大
int dis = depth[a] - depth[b]; //深度差
for (register int i = 0; (1 << i) <= dis; ++i)
if ((1 << i) & dis) a = f[a][i];
然后我们就要从下面往上面跳,但是每次跳不能跳到从两个点都可以跳到的地方,要不然可能会跳到LCA上面去,所以我们每次都是尽量跳到接近LCA的地方,这样之后可以证明当每次这么跳最后没法跳的时候所到达的点的父亲就是LCA。
接下来就是完整的求LCA的代码:
int lca(int a, int b) {
if (depth[a] < depth[b]) swap(a, b);
int dis = depth[a] - depth[b];
for (register int i = 0; (1 << i) <= dis; ++i)
if ((1 << i) & dis) a = f[a][i];
if (a == b) return a;
for (register int i = 25; i >= 0; --i) {
if (f[a][i] != f[b][i])
a = f[a][i], b = f[b][i];
}
return f[a][0];
}
一个模板题目:
题目地址:
题目大意:
给你一个树,然后让你求几对结点的LCA
注意事项:
- 这个地方不能使用
vector
来存图,要不然会超时 - 用数组存边的时候要开结点数量两倍的空间,因为我们存的是一个无向图要存反向边。
题目代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 5e5 + 5;
int n, m, s;
int u, v;
int x, y;
int head[N]; //每个点连出去的第一条边的编号
int cnt = 0; //边的数量
int depth[N]; //每个边的深度
int vis[N]; //判断每个点是否走过
int f[N][50];
struct EDGE {
int s; //这个边的起点
int e; //这个边的终点
int w; //这个边的权值
int nxt; //这个边的下一条边的指针
} Edge[N * 2];
void add(int u, int v, int w) {
++cnt;
Edge[cnt].s = u;
Edge[cnt].e = v;
Edge[cnt].w = w;
Edge[cnt].nxt = head[u];
head[u] = cnt;
}
void dfs(int x, int dep) {
for (register int i = 1; i < 25; ++i)
f[x][i] = f[f[x][i - 1]][i - 1];
for (register int i = head[x]; i != 0 && Edge[i].s == x; i = Edge[i].nxt) {
if (vis[Edge[i].e]) continue;
vis[Edge[i].e] = true;
f[Edge[i].e][0] = x;
depth[Edge[i].e] = dep + 1;
dfs(Edge[i].e, dep + 1);
}
}
int lca(int a, int b) {
if (depth[a] < depth[b]) swap(a, b);
int dis = depth[a] - depth[b];
for (register int i = 0; (1 << i) <= dis; ++i)
if ((1 << i) & dis) a = f[a][i];
if (a == b) return a;
for (register int i = 25; i >= 0; --i) {
if (f[a][i] != f[b][i])
a = f[a][i], b = f[b][i];
}
return f[a][0];
}
int read() {
int s = 0, w = 1;
char ch = getchar();
while (ch <='0' || ch > '9') {
if (ch == '-') w = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
s = s * 10 + ch - '0';
ch = getchar();
}
return s * w;
}
inline void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
int main(int argc, char *argv[]) {
scanf("%d %d %d", &n, &m, &s);
for (register int i = 1; i < n; ++i) {
scanf("%d %d", &u, &v);
add(u, v, 1);
add(v, u, 1);
}
vis[s] = true;
dfs(s, 0);
for (register int i = 1; i <= m; ++i) {
scanf("%d %d", &x, &y);
printf("%d\n", lca(x, y));
}
return 0;
}