c++最近公共祖先LCA

文章介绍了C++中求解无环树上两个节点最近公共祖先(LCA)的算法,从暴力实现的O(n)时间复杂度,过渡到使用倍增算法优化到O(logn)。通过深搜和动态维护节点的2^i层祖先信息,实现高效的LCA查询。此外,文章还提供了一个例题来详细解释算法的应用,并提到LCA在计算树上两点最短距离的特殊用途。
摘要由CSDN通过智能技术生成

目录

介绍

暴力实现

优化

例题

总结

特殊用处


介绍

今天来介绍一个c++中的算法:最近公共祖先LCA。

最近公共祖先是什么呢?就是给出在一棵没有环的树上的两个节点,求出它们的最近公共祖先,也可以理解成深度最深的公共祖先。这个算法用处很大,比如我们可以用它来求任意两个节点的距离。具体后面会讲。


暴力实现

我们该怎么实现呢?

首先,我们要知道一个结点的所有祖先,就是他的父亲、父亲的父亲、父亲的父亲的父亲......

然后,我们先用最暴力的思路想一下:公共祖先,那我只要把两个点的所有祖先都找出来,然后有重复的不就是公共祖先了吗。至于最近,我们只需要将第一个重复的祖先输出就行了。

 所以我们有了最暴力的做法:先把一个点的所有祖先找到并做上标记,然后找一个点的所有祖先,发现有标记就成功找到了最近公共祖先。

不过这种算法是很劣的,时间复杂度是\mathcal{O}(n)的,一般情况下都过不了。所以我们就得想办法优化。


优化

怎么优化呢?下面我们就要引入一个知识:倍增。

我们可以看到,其实我们没有必要把所有的祖先都枚举做上标记,这样是很浪费时间的。举个例子,如果一个悬崖边上有一个宝箱,但你又不知道你距离悬崖口有多远,这时候你一步一步往前走肯定是很累的。假设你距离悬崖边13步,而你只有在距离悬崖边一步时才能看见悬崖边。这时候我们就得想一个既有效率又安全的走法。

我们可以抛石子来确定距离。第一次抛16步的距离,石子掉下去了。第二次抛8步的距离,石子没掉下去,那我们就往前走八步。第三次抛4步远的距离,我们发现可以往前走四步。现在距离悬崖只有1步了,可以发现悬崖和宝藏就在我们眼前。这样本来要走13步的路程,我们只走了3步,如果运气好第一次就抛8步那么就只要走两步。

向上面每次将距离除以2的方法就是倍增。

为什么这个方法可行呢?我们可以用2进制来解释。我们知道,所有十进制数都可以用二进制来表示,那么所有的距离我们都用二进制来表示。那我们就可以把每次调的距离当作加数,把问题转化成求几个二进制数相加等于一个二进制数。比如说 n = 1100101(2)这个数。

我们定第一个加数为A1 = 1000000,发现没有超过n,那就将加数除以二,也就是看下一位。加数变成了A2 = 100000,加上A1,没有超过n,除以二,看下一位。A3等于10000,加上前两个加数,现在的家属和变成了1110000,超过了n,所以这一位必须等于零,加数除以二。以此类推,我们分别加上了1000000,100000,100和1。这就代表我们在十进制中加上了32,16,4和1。这也就说明了每一次跳的距离都要除以二。

 但是为什么要从大到小呢?为了方便理解,我们再回到悬崖上。还记得距离是13吧,在二进制中就是1101。

如果从小到大,第一次,我们选择的是1,发现可以。

第二次,我们跳2,也就是10,发现没有超过1101,但是1101中这一位却是0,我们就跳多了,到后面还需要回溯,也就是往回跳,明显不是最优。

 所以在不知道具体距离的情况下我们需要从大往小减小距离。

倍增的时间复杂度是\mathcal{O}(logn)的,完完全全可以过。知道了这个前置知识,我们就可以开始学习LCA了。


例题

我们结合例题来详细讲解此算法吧。

复制一个题目:

题目描述

如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

输入格式

第一行包含三个正整数 N,M,S 分别表示树的结点个数、询问的个数和树根结点的序号。

接下来 N-1 行每行包含两个正整数 x, y 表示 x 结点和 y 结点之间有一条直接连接的边(数据保证可以构成树)。

接下来 M 行每行包含两个正整数 a, b 表示询问 a 结点和 b 结点的最近公共祖先。

输出格式

输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。

输入输出样例

输入 #1

5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5

输出 #1

4
4
1
4
4

说明/提示

对于 30% 的数据,N≤10,M≤10。

对于 70% 的数据,N≤10000,M≤10000。

对于 100% 的数据,1≤N,M≤500000,1≤x,y,a,b≤N,不保证 a≠b。

首先,我们需要存图。很多大佬喜欢用前向星存图,但是前向星比 vector 其实快不了多少。我的建议是考试时第一遍先用 vector 寸土,写完后离考试结束还有充足的时间,那就再用前向星改一遍。因为 vector 写起来代码量要比前向星少很多,并且还很方便查错。(主要还是因为我懒)。但是考试时一定要选择好方法,万一(无良)出题人就想卡 vector 呢。

这里我们就用 vector 存图。

cin >> n >> m >> s;
	for(int i = 1; i < n; i++){
		cin >> x >> y;
		edge[x].push_back(y);
		edge[y].push_back(x);
	}

由于不知道树长什么样,所以我们需要正过来存一编,反过来再存一遍。也就是把它当作无向图来存储。但这有可能出现一个问题,就是一个点的儿子有可能成为它的父亲,则时候就需要特判了,后面再讲。

然后我们需要把所有结点的祖先都找出来,就是用 dfs 遍历一遍树,用 fa[i][j] 表示节点 i 向上 2^j 级父亲,具体解释如图。

这样我们就可以表示每个节点 2^i 级的父亲了,这也方便后面的倍增。

void dfs(int now, int father){
	deep[now] = deep[father] + 1;
	for(int i = 1; (1 << i) <= deep[now]; i++){//i从1开始! 2^i <= 现在的深度,代表最高一级的父亲不能超过根节点
		fa[now][i] = fa[fa[now][i - 1]][i - 1];
	}
	for(int i = 0; i < edge[now].size(); i++){
		if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了 
			fa[edge[now][i]][0] = now;
			dfs(edge[now][i], now);
		}
	}
}

首先,当前的节点深度是父亲节点深度加1。然后获得 i 节点的所有 2^i 祖先节点。从 2^1 祖先开始,一直到根节点,也就是 2^i ≤ deep[now]。

那为什么 fa[now][i] = fa[fa[now][i - 1]][i - 1] 呢?我们知道,2^(i - 1) + 2^(i - 1) = 2^i,所以用 now 的 2^(i - 1) 级父亲的 2^(i - 1) 级父亲就得到了 now 的 2^i 级祖先。

 我们从根节点开始深搜,就可以获得每个结点所需要的数据了。其中 deep 数组存储的是每个节点的深度。

然后就是求两个给定节点的最近公共祖先。

for(int i = 1; i <= m; i++){
		cin >> x >> y;
		cout << LCA(x, y) << endl;
	} 

LCA() 函数该怎么写呢?我们有了倍增的思路,那具体该怎么实现呢?首先,我们需要把两个节点的深度调到同一高度,这样做就可以让两个节点一起向上倍增,很方便。

让两个结点的深度相同,我们就把深度更深点的向上调。首先需要获取两个节点的深度差。

if(deep[x] < deep[y]) swap(x, y);
	int h = deep[x] - deep[y];

然后不断让 x = fa[x][i]。

for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级 
	if(h & 1){
		x = fa[x][i];
	}
}

有人就要问了,那么这次为什么可以从小到大呢?因为我们已经知道了 h 的具体值,这样就可以准确地知道第几次跳,第几次不跳,具体做法就是每次用 h&1,在二进制中如果 h 的这一位是1,那么按位与够的结果就是1,就代表可以跳,并将 h 右移一位。i 就是指数。这样就实现了将 x 向上调高度与 y 相平。

然后将两个点一起向上倍增。首先要特判,如果两个点在同一高度后重合了,那么就说明它本身就是最近的公共祖先,直接返回 x 就好了

if(x == y) return x;

然后就从大到小向上。但是最大是多少呢?题目中说 1≤N≤500000 ,所以 2^i 最多不超过500000,也就是说 i 不超过20。所以循环起点就有了。

for(int i = 20; i >= 0; i--){
	if(fa[x][i] != fa[y][i]){
		x = fa[x][i];
		y = fa[y][i];
	}
}

咦,怎么 (fa[x][i] != fa[y][i]) 呢?因为如果向上直接就相等了,那么我们并不知道是否是最近的共公共祖先,所以如果相等那也不能加。

到了最后该怎么办呢?我们直接返回 fa[x][0] 就好了,为什么呢?其实所有二进制数最后一位只有 1 或 0(废话)。那么如果是1的话,由于循环中的判断条件,最后一位不会跳,因为跳了就相等了。如果是0的话,那么最后一个是1的一位不会跳,因为如果跳了就相等了,那么后面所有0都会跳。比如:10100。倒数第三位不能跳,后两位必须跳。那么就变成了:10011,可以看到,跳的数比原数要小一。

综上所述,两个情况下都要比真正的距离小一。所以我们只要返回 fa[x][0]。

所以LCA()函数就变成了这样:

int LCA(int x, int y){
	if(deep[x] < deep[y]) swap(x, y);
	int h = deep[x] - deep[y];
	for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级 
		if(h & 1){
			x = fa[x][i];
		}
	}
	if(x == y) return x;
	for(int i = 20; i >= 0; i--){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}

这样,我们就能求得任意两个节点的最近公共祖先了。


总结

#include<bits/stdc++.h>
using namespace std;
const int N = 500010;
int n, m, s, fa[N][22], deep[N], x, y;
vector<int>edge[N * 2];
void dfs(int now, int father){
	deep[now] = deep[father] + 1;
	for(int i = 1; (1 << i) <= deep[now]; i++){//i从1开始! 
		fa[now][i] = fa[fa[now][i - 1]][i - 1];
	}
	for(int i = 0; i < edge[now].size(); i++){
		if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了 
			fa[edge[now][i]][0] = now;
			dfs(edge[now][i], now);
		}
	}
}
int LCA(int x, int y){
	if(deep[x] < deep[y]) swap(x, y);
	int h = deep[x] - deep[y];
	for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级 
		if(h & 1){
			x = fa[x][i];
		}
	}
	if(x == y) return x;
	for(int i = 20; i >= 0; i--){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}
int main(){
	cin >> n >> m >> s;
	for(int i = 1; i < n; i++){
		cin >> x >> y;
		edge[x].push_back(y);
		edge[y].push_back(x);
	}
	dfs(s, 0);
	for(int i = 1; i <= m; i++){
		cin >> x >> y;
		cout << LCA(x, y) << endl;
	} 
	return 0;
}

这是LCA的模板,建议收藏。

我们再来总结一下。

首先,用 vector 存图,由于不知道树长什么样,所以要当成无向图来存储。

然后用深搜得到所有节点的深度和 2^i 父亲。注意:由于存图时存了两个方向,所以一个节点的儿子有可能一跃成为他的父亲,所以需要判断一下。

if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了 
	fa[edge[now][i]][0] = now;
	dfs(edge[now][i], now);
}

然后找两个节点的最近公共祖先。先把 x 变为两个节点中深度更深的那一个,方便操作。获得两个点的深度差后将 x 的深度变成和 y 一样的。

for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级 
	if(h & 1){
		x = fa[x][i];
	}
}

记住特判!如果 x 本身就是最近公共祖先,直接返回。

然后将 x 和 y 同时向上,如果有公共祖先就将距离除以2。最后返回 fa[x][0],也就是 x 的父亲。


特殊用处

其实LCA还有很多特殊用处,这里只介绍一种,就是它可以求出任意树上两个点的最短距离。

mindis(a, b) = deep(a) + deep(b) - 2 * deep(LCA(a, b))

为什么呢?我们可以这样理解,假设 q 点是 a、b 的最近公共祖先,那么就有

mindis(a, b) = deep(a) - deep(q) + deep(b) - deep(q)

就像这样。所以我们求出了两点的最近公共祖先,进一步就可以求出两点之间的最短距离。

LCA还有很多实现方法和用处,这里就不一一阐述。


如果对于LCA()还有不懂的,可以来评论区问我。

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值