3

题解 最近公共祖先 (LCA)

 2 years ago
source link: https://aeilot.github.io/2021/04/24/Luogu-P3379/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

链接: 洛谷 P3379

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

818487-20151004150339121-181913844.png

818487-20151004150339121-181913844.png


图中,4 和 3 的 LCA 就是 1。

最简单的方法 (暴力)

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

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

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

#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$ 步向上跳,一直到相遇。

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

#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

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

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK