题解 最近公共祖先 (LCA)

好久没刷题了,复习一下:LCA。

题目详情

题目很简单,就是求多叉树两个点的最近公共祖先。

链接: 洛谷 P3379

LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———来自百度百科

818487-20151004150339121-181913844.png
图中,4 和 3 的 LCA 就是 1。

解题

最简单的方法 (暴力)

这种方法数据一大就会TLE。

原理很简单,让两个数一个一个向上走,直到两个数相遇。第一次相遇就是他们的 LCA。

很简单,就不赘述了,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <cmath>
#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;

#define MAX 500000

vector<int> tree[MAX];// 以邻接表形式存储
int dep[MAX];
int fas[MAX];

namespace M1 {
void dfs(int x, int fa) {
if (fa != -1) {
dep[x] = dep[fa] + 1;
}
fas[x] = fa;
for (int i = 0; i < tree[x].size(); i++) {
if (fas[tree[x].at(i)] == -2) M1::dfs(tree[x].at(i), x);
}
}


int solve(int a, int b) {
while (1) {
if (a == b) {
return a;
} else if (fas[a] == fas[b]) {
return fas[a];
} else if (fas[b] == a) {
return a;
} else if (fas[a] == b) {
return b;
}
int da = dep[a], db = dep[b];
int delta = abs(da - db);
if (da > db) {
for (int i = 0; i < delta; i++) {
a = fas[a];
da = dep[a];
}
} else if (da < db) {
for (int i = 0; i < delta; i++) {
b = fas[b];
db = dep[b];
}
} else {
a = fas[a];
da = dep[a];
}
}
return -1;
}
}// namespace M1

bool first = true;

int LCA(int a, int b, int r) {
if (first) {
M1::dfs(r, -1);
first = false;
}
int res;
res = M1::solve(a, b);
return res;
}

int main(int argc, char *argv[]) {
int m, n, s;
cin >> m >> n >> s;
for (int i = 0; i < MAX; i++) {
dep[i] = 0;
fas[i] = -2;
}
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
tree[x].push_back(y);
tree[y].push_back(x);
}
dep[s] = 0;
fas[s] = -1;
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
int res = LCA(a, b, s);
cout << res << endl;
}
return 0;
}

注意这道题目的数据输入,x y 表示x 结点和 y 结点之间有一条直接连接的边(数据保证可以构成树)。 所以需要用邻接表的形式,表示多叉树。

倍增法

这个算法是对上面暴力算法的优化。这个算法的时间复杂度为$O(nlogn)$,已经可以满足大部分的需求。

上述算法中,一步一步跳太慢了,这里我们事先做好标记,就可以每次 $2^i$ 步向上跳,一直到相遇。

代码中有较为详细的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <cmath>
#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;

#define MAX 500001 // 本题最大数据规模
#define MUL_MAX 22

vector<int> tree[MAX]; // 以邻接表形式存储
int dep[MAX]; // 预处理存储节点深度
int fas[MAX][MUL_MAX]; // 存储 x 节点上面第 2^i 次方个祖先
bool first = true; // 记录预处理是否结束

int lg2(int x) {
return log(x) / log(2) + 1; // 无理数计算记得加上 1 来避免误差
} // 手动写了一个函数,来求 log2(x)

void dfs(int x, int fa) { // x 是当前节点,fa 是它的父节点
if (fa != -1) {
dep[x] = dep[fa] + 1; // x 的深度就是它的父节点加一,这很好理解
}
fas[x][0] = fa; // x 节点的第一个父节点是 fa
for (int i = 1; (1 << i) <= dep[x]; i++) { // 循环直至 2^i 大于当前节点深度,即完成当前节点到树根的所有可跳转到的节点的预处理工作
fas[x][i] = fas[fas[x][i - 1]][i - 1];
// 这一步是算法的精髓
// 得到状态转移方程,动态规划计算
// 意思是x的2^i祖先等于x的2^(i-1)祖先的2^(i-1)祖先
}
for (int i = 0; i < tree[x].size(); i++) {
if (tree[x].at(i) != fa) dfs(tree[x].at(i), x);
// 邻接表存储当前节点所有相连的节点,只要节点不是它的父节点,即节点是它的子节点,就进行下一步递归
}
} // 深度优先搜索来预处理一下

int solve(int a, int b) {
if (dep[b] > dep[a]) swap(a, b); // 确保 a 的深度更深,避免冗余的判断
while (dep[a] > dep[b]) {
a = fas[a][lg2(dep[a] - dep[b]) - 1]; // a 向上跳,跳至两节点同级
}
if (a == b) return a; // 若此时两节点相遇,就可以直接返回。否则两节点还需再次向上跳。
for (int i = lg2(dep[a]); i >= 0; i--) {
if (fas[a][i] != fas[b][i]) {
a = fas[a][i];
b = fas[b][i];
}
} // 从可跳到的最高处向下枚举,得到 LCA
return fas[a][0]; // 返回答案
}

int LCA(int a, int b, int r) {
if (first) {
dep[r] = 0;
fas[r][0] = -1;
dfs(r, -1);
first = false;
}
int res;
res = solve(a, b);
return res;
}

int main(int argc, char *argv[]) {
int m, n, s;
cin >> m >> n >> s;
for (int i = 0; i < MAX; i++) {
dep[i] = 0;
for (int j = 0; j < MUL_MAX; j++)
fas[i][j] = -2;
} // 数组初始化
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
tree[x].push_back(y);
tree[y].push_back(x);
// 邻接表存入数据
}
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
int res = LCA(a, b, s);
cout << res << endl;
}
return 0;
}

其实还可以预处理出一个 lg 数组,避免对数计算,大家可以自己去尝试,会有一定时间优化效果。

无法理解倍增?这里有个 经典资料

其他方法

实际上还有更快的方法求这道题的答案。倍增算法已经可以满足需求,就不再往下写了。

  • Tarjan
  • ST 算法

大家有兴趣可以去尝试一下。

后记

这里放上两种列出算法的评分。

暴力

截屏2021-04-24 下午6.13.14.png

倍增

截屏2021-04-24 下午6.13.08.png