算法学习:最近公共祖先

这篇博客探讨了三种不同的算法来寻找树中两个节点的最近公共祖先,包括暴力算法、倍增思想和Tarjan算法。每种方法都有其时间复杂度和实现原理,并通过代码示例进行了详细解释。暴力算法复杂度为O(m*logn),倍增思想算法复杂度为O(nlog(n)+mlog(n)),而Tarjan算法复杂度为O(n+m)。这些算法在处理大规模数据时能有效提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近公共祖先

1、暴力算法

复杂度:O(m * logn)(最大O(n * m));

先记录每个层数,再对层数高的依次向上移动,直到找到相同祖先,此处不写代码

2、倍增思想

复杂度:O(nlog(n)+mlog(n));

原理:

记录其第2^i个父节点的内容,按二进制进位行运算

首先初始化sic[x] [0]为其上第一个父节点,随后从i到所需1<<n,依次根据上一层父节点的父节点进行记录:

sic[x][i] = sic[sic[x][i - 1]][i - 1];

查找最近公共祖先时,首先将两结点放在同一层

for (int i = 15; i > -1; i--){
	if (len[sic[a][i]] >= len[b])a = sic[a][i];
}

因为父节点层数一定比a与b的最小层数小或相等,因此不会出现有跳过父节点的情况。

之后,同时将a与b进行跳跃操作,跳跃到最近公共父节点的下一层(因每次跳跃的判定条件是)sic[a] [i] != sic[b] [i],因此只能找到最近公共父节点下一层,最后返回sic[a] [0]即可得到最近公共祖先。

代码如下:
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<string>
#include<string.h>
#include<vector>
#include<queue>

using namespace std;

int n, m;
int sic[1005][16], len[1005];
vector<int>v[1005];

void lca() {
	queue<int>q;
	q.emplace(1);
	while (!q.empty()) {
		int k = q.front();
		q.pop();
		for (auto x : v[k]) {
			len[x] = len[k] + 1;
			for (int i = 1; i < 15; i++){
				if (sic[sic[x][i - 1]][i - 1] == 0)break;
				sic[x][i] = sic[sic[x][i - 1]][i - 1];
			}
			q.emplace(x);
		}
	}
}

int solve(int a,int b) {
	if (len[a] < len[b])swap(a, b);
	for (int i = 15; i > -1; i--){
		if (len[sic[a][i]] >= len[b])a = sic[a][i];
	}
	if (a == b)return a;
	for (int i = 15; i > -1; i--) {
		if (sic[a][i] != sic[b][i]) {
			a = sic[a][i]; b = sic[b][i];
		}
	}
	return sic[a][0];
}

int main() {
	int a, b;
	cin >> n >> m;
	memset(sic, -1, sizeof sic);
	sic[1][0] = -1;
	len[1] = 1;
	for (int i = 0; i < n; i++){
		cin >> a >> b;
		v[a].push_back(b);
		sic[b][0] = a;
	}
	lca();
	for (int i = 0; i < m; i++){
		cin >> a >> b;
		cout << solve(a, b) << endl;
	}
	return 0;
}
样例:

7 6

1 2 2 3 3 4 2 5 5 6 6 7 1 8

4 7

3 2

5 2

1 8

7 8

4 6

预期输出:

2

2

2

1

1

2

3、Tarjan

复杂度:O(n+m);

原理:

此算法可算作是暴力求解算法的优化,是离线查询,其原理如下:

将树上的点分为三个部分:

【1】已经遍历且回溯过的点,如蓝色部分

【2】正在遍历且未回溯的点,如红色分支

【3】还未遍历的点

如下图所示:

在这里插入图片描述

此时,与并查集的思想相结合,已经回溯过的点,其根结点记录在目前正在遍历分支上。当要查询的结点中的一个被遍历到,且另一个点已经被回溯过,则直接找到另一个点的根节点,就是这两个点的最近公共祖先。

那么,如何记录已经回溯过的点在目前正在遍历的点的根节点?

首先初始化每个结点的根节点是其本身,在每次回溯过后,再更改其根节点位置,如此,其已回溯过的分支的根节点在其回溯之前的根节点一定是此结点本身,在遍历此节点的子节点分支时,这个状态不会改变。

在这里插入图片描述

在这里插入图片描述

如上图所示,当回溯到3时,3的根节点更新为其父节点。

根据如上原理,就可有如下操作:

void tarjan(int u) {
	sic[u] = 1;
	for (auto x:v[u]){
		//st[x] = st[u] + 1;
		tarjan(x);
		len[x] = u;
	}
	for (auto x : vis[u]) {
		int y = x.first, p = x.second;
		if (sic[y] == 2) {
			ans[p] = fa(y);
		}
	}
	sic[u] = 2;
}

sic数组为标记数组,标记点是否被遍历/已回溯/未遍历;

len数组为记录数组,记录其根节点,在每次回溯后更新当前回溯点的根节点。

注:此代码建立的是有向图,因此不须剪枝。

代码如下:
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<string>
#include<string.h>
#include<vector>
#include<queue>

using namespace std;

int n, m;
int sic[1005], len[1005];
int st[1005];
int ans[1005], o;
vector<int>v[1005];
vector<pair<int,int>>vis[1005];

int fa(int x) {
	if (len[x] != x)len[x] = fa(len[x]);
	return len[x];
}

void tarjan(int u) {
	sic[u] = 1;
	for (auto x:v[u]){
		//st[x] = st[u] + 1;
		tarjan(x);
		len[x] = u;
	}
	for (auto x : vis[u]) {
		int y = x.first, p = x.second;
		if (sic[y] == 2) {
			ans[p] = fa(y);
		}
	}
	sic[u] = 2;
}

int main() {
	int a, b;
	cin >> n >> m;
	len[1] = 1;
	for (int i = 0; i < 1005; i++){
		len[i] = i;
	}
	for (int i = 0; i < n; i++){
		cin >> a >> b;
		v[a].push_back(b);
	}
	for (int i = 0; i < m; i++){
		cin >> a >> b;
		vis[a].push_back({ b,i });
		vis[b].push_back({ a,i });
	}
	//st[1] = 0;
	tarjan(1);
	for (int i = 0; i < m; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值