最近公共祖先

最近公共祖先简称LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点所有的公共祖先里面,离根最远的那个。

  • d(u,v)=h(u)+h(v)2h(lca(u,v))

例题
P3379 【模板】最近公共祖先(LCA)

Tarjan求解最近公共祖先

基本思路
1.dfs整棵树
2.每到达一个结点,先标记这个节点已被访问
3.随后遍历与该结点又访问关系的结点(记为z),若z已经被访问过,那么二者的lca就是getf(z)。
4.接着向孩子结点遍历,回溯的时候将孩子结点与当前结点用并查集合并。
参考代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
vector<int>node[N];
int f[N];
int getf(int a){
	return f[a]==a?a:f[a]=getf(f[a]);
}
int n,m,s;
int book[N];
struct{
	int to,next;
}e[2*N];
int head[N],cnt;
inline void add(int a,int b){
	e[++cnt].to=b;
	e[cnt].next=head[a];
	head[a]=cnt;
}
int ans[N];
void Tarjan(int s){
	book[s]=1;
	int to;
	for(int i=head[s];i;i=e[i].next){
		to=e[i].to;
		if(book[to])ans[(i+1)/2]=getf(to);
	}
	for(auto i:node[s]){
		if(book[i])continue;
		Tarjan(i);
		f[i]=s;
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	int a,b;
	for(int i=1;i<=n;++i)f[i]=i;
	for(int i=1;i<n;++i){
		scanf("%d%d",&a,&b);
		node[a].push_back(b);
		node[b].push_back(a);
	}
	for(int i=1;i<=m;++i){
		scanf("%d%d",&a,&b);
		add(a,b);
		add(b,a);
	}
	Tarjan(s);
	for(int i=1;i<=m;++i)printf("%d\n",ans[i]);
}

倍增

倍增是对朴素算法的优化。对于要求解的两个点a,b(假设a的深度大于b),先将两个结点上升到同一高度,然后再一起向上跳,这个过程用二进制就很快了。
参考代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int M=5e5+10;
struct{
	int to,next;
}e[2*M];
int head[N],cnt,dep[N];
int n,m,s;
inline void add(int a,int b){
	e[++cnt].to=b;
	e[cnt].next=head[a];
	head[a]=cnt;
}
int len;
int fa[N][30];//fa[i][j]表示i结点的2^j个祖先
void dfs(int x,int f){
	fa[x][0]=f;
	dep[x]=dep[f]+1;
	int to;
	for(int i=1;(1<<i)<=dep[x];++i)fa[x][i]=fa[fa[x][i-1]][i-1];//这是fa的转移方程
	for(int i=head[x];i;i=e[i].next){
		to=e[i].to;
		if(to==f)continue;
		dfs(to,x);
	}
}
int lca(int a,int b){
	if(dep[a]<dep[b])swap(a,b);
	int dif=dep[a]-dep[b];
	int t=0;
	//printf("%d\n",dif);
	while(dif){
		if(dif&(1<<t))a=fa[a][t],dif-=(1<<t);
		++t;
	}
	//printf("%d %d\n",a,b);
	if(a==b)return a;
	for(int i=len;i>=0;--i){//这里可以直接理解为二分查找
		if(fa[a][i]!=fa[b][i]){
			a=fa[a][i];
			b=fa[b][i];
		}
	}
	return fa[a][0];
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	int a,b;
	while((1<<len)<=n)++len;
	for(int i=1;i<n;++i){
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}	
	dfs(s,0);
	for(int i=1;i<=m;++i){
		scanf("%d%d",&a,&b);
		printf("%d\n",lca(a,b));
	}
	return 0;
}

基于DFS序的倍增

众所周知,DFS序可以很快的判断出两个节点的祖孙关系。
我们只需将深度大的点一直向上跳(还是基于二进制或二分的思想跳),并不断判断祖孙关系即可。
这种方法不用将两个点升到同一高度,并且常数更小。
参考代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
const int M=5e5+10;
struct{
	int to,next;
}e[2*M];
int head[N],cnt,dep[N];
int n,m,s;
inline void add(int a,int b){
	e[++cnt].to=b;
	e[cnt].next=head[a];
	head[a]=cnt;
}
int len;
int fa[N][30];
int dfn[N],se[N],ti;
void dfs(int x,int f){
	fa[x][0]=f;
	dep[x]=dep[f]+1;
	int to;
	dfn[x]=++ti;
	se[x]=1;
	for(int i=1;(1<<i)<=dep[x];++i)fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=head[x];i;i=e[i].next){
		to=e[i].to;
		if(to==f)continue;
		dfs(to,x);
		se[x]+=se[to];
	}
}
int lca(int a,int b){
	if(dep[a]<dep[b])swap(a,b);
	int dif=dep[a]-dep[b];
	int t=0;
	if(dfn[b]<=dfn[a]&&dfn[b]+se[b]-1>=dfn[a])return b;
	for(int i=len;i>=0;--i){
		int na=fa[a][i];
		if(!na)continue;//这个地方小心跳穿了,因为深度是从1开始的
		if(!(dfn[na]<=dfn[b]&&dfn[na]+se[na]-1>=dfn[b]))
		a=na;
	}
	return fa[a][0];
	if(a==b)return a;
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	int a,b;
	while((1<<len)<=n)++len;
	for(int i=1;i<n;++i){
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}	
	dfs(s,0);
	for(int i=1;i<=m;++i){
		scanf("%d%d",&a,&b);
		printf("%d\n",lca(a,b));
	}
	return 0;
}
posted @   何太狼  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示