最近公共祖先(LCA)

本文介绍了最近公共祖先(LCA)的概念,并详细讲解了四种算法:向上标记法、树上倍增法、Tarjan算法和树链剖分,其中重点阐述了每种算法的时间复杂度和实现思路,提供了相关题目链接供实践。
摘要由CSDN通过智能技术生成

【概念】

       给定一棵树,若节点 z 既是结点 x 的祖先,也是节点 y 的祖先,并且在 x 和 y 的祖先中深度最大,称为 x , y 的 最近公共祖先(Lowest Common Ancestors)。

【算法】

1.向上标记法

       从 x 向上走到根节点,并标记所有经过的点。

       从 y 向上走到根节点,第一次遇到的已标记的节点即为 x 与 y 的 最近公共祖先 。

       对于每个询问的时间复杂度为 O(n)。

<代码实现> 

       [洛谷P3379]

inline void dfs(int x,int fa){
	for(int i=lin[x];i;i=e[i].Next){
		if(e[i].Id==fa) continue;
		Fa[e[i].Id]=x;
		Dep[e[i].Id]=Dep[x]+1;
		dfs(e[i].Id,x);
	}
}
inline void Add_Tag(int x){
	for(;Fa[x];++T[x],x=Fa[x]);
}
inline void Del_Tag(int x){
	for(;Fa[x];--T[x],x=Fa[x]);
}
inline int Find_Lca(int x){
	for(;Fa[x]&&!T[x];x=Fa[x]);
	return x;
}
inline void Work(){
	dfs(S,0);
	for(int i=1;i<=M;i++){
		int x,y; scanf("%d%d",&x,&y);
		Add_Tag(x);
		printf("%d\n",Find_Lca(y));
		Del_Tag(x);
	}
} 

2.树上倍增法

       设 f[i][j] 代表从编号为 i 的节点向上走 2^j 步到达的节点编号为 f[i][j] 。若该节点不存在,则令 f[i][j]=0 (令 f[i][j]=-1 也可以,但是需要注意以 f[i][j] 作下标可能越界)。

       若  x 与 y 不在同一深度,则利用二进制拆分思想 将 x 与 y 调整至同一深度 

       若 此时 x = y ,则已经找到 LCA, LCA(x,y)= x  。

       否则 用二进制拆分思想,将 x 与 y 同时向上调整,并保持 x!= y 。

 

       设 x , y 向上移动D步所到达的节点为他们最近公的共祖先。

       移动 x ,y 我们可以发现:

       当已经枚举到第 k 位,通过一系列累加和(0/1*2^max_k+0/1*2^(max_k-1)+.....0/1*2^(k+1) )所得到的和为D'。

       若 D'+2^k < D 则 x, y 向上走 D'+2^k 步到达的节点不是他们的公共祖先,即 f[x][k] != f[y][k] (x 与 y 在不断往上更新)。

       若D'+2^k >= D 则 x, y 向上走D'+2^k 步到达的节点时他们的公共祖先,即 f[x][k] = f[y][k] 

       所以,当 f[x][k] != f[y][k] 时 x=f[x][k],y=f[y][k] (相当于D'+2^k) 我们保证了最后求的的 D' 比 D 小 1。

        时间复杂度为 O((n+m)log n)

<代码实现>

       [洛谷P3379]

inline void dfs(int x,int fa){
	for(int i=lin[x];i;i=e[i].Next){
		if(e[i].Id==fa) continue;
		Fa[e[i].Id]=x;
		Dep[e[i].Id]=Dep[x]+1;
		dfs(e[i].Id,x);
	}
}
inline int Lca(int x,int y){
	if(Dep[x]<Dep[y]) x^=y^=x^=y; 
	for(int i=lg;i>=0;i--)
	 if(Dep[f[x][i]]>=Dep[y])
	  x=f[x][i];
//    for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
//	 if(D&1)
//	  y=fa[y][i];//二进制拆分往上挪 
	if(x==y) return x;
	for(int i=lg;i>=0;i--)
	 if(f[x][i]!=f[y][i])
	  x=f[x][i],y=f[y][i];
	return f[x][0];
}
inline void Work(){
	dfs(S,0);
	for(int i=1;i<=N;i++) f[i][0]=Fa[i];
	lg=(int)(log(N)/log(2))+1;
	for(int i=1;i<=lg;i++)
	 for(int j=1;j<=N;j++)
	  f[j][i]=f[f[j][i-1]][i-1];
	for(int i=1;i<=M;i++){
		int x,y; scanf("%d%d",&x,&y);
		printf("%d\n",Lca(x,y));
	}
} 

3.Tarjan算法(离线)

       Tarjan算法基本上使用并查集对“向上标记法”的优化。将 m 个询问一次性读入,统一计算。

       时间复杂度为:O(n+m)

<代码实现>

       [洛谷P3379]

inline int get(int x){
	return x==Fa[x]?x:Fa[x]=get(Fa[x]);
}
inline void Tarjan(int x,int fa){
	for(int i=lin[x];i;i=e[i].Next){
		if(e[i].Id==fa) continue;
		Tarjan(e[i].Id,x);
		Fa[e[i].Id]=x;
	}
	for(int i=Lin[x];i;i=E[i].Next){
		if(vis[E[i].Id]){
			Ans[E[i].v]=get(E[i].Id);
		}
	}
	++vis[x];
}
int main(){
    ...
	for(int i=1;i<=M;i++){
		int x,y; scanf("%d%d",&x,&y);
		Insert(x,y,i); Insert(y,x,i);//Lin & E 
	}
	for(int i=1;i<=N;i++) Fa[i]=i;
	Tarjan(S,0);
	...
}

4.树链剖分

      跳到同一重链,答案即为深度较浅的点

       [洛谷P3379]

inline void dfs(int x,int fa){
	Size[x]=1;
	for(int i=lin[x];i;i=e[i].Next){
		if(e[i].Id==fa) continue;
		Dep[e[i].Id]=Dep[x]+1;
		Fa[e[i].Id]=x;
		dfs(e[i].Id,x);
		Size[x]+=Size[e[i].Id];
		if(Size[e[i].Id]>Size[Son[x]]) Son[x]=e[i].Id;
	}
}
inline void DFS(int x,int TOP){
	Top[x]=TOP;
	if(Son[x]) DFS(Son[x],TOP);
	for(int i=lin[x];i;i=e[i].Next){
		if(e[i].Id==Fa[x]||e[i].Id==Son[x]) continue;
		DFS(e[i].Id,e[i].Id);
	}
}
inline int Lca(int x,int y){
	for(;Top[x]^Top[y];x=Fa[Top[x]])
	 if(Dep[Top[x]]<Dep[Top[y]]) x^=y^=x^=y;
	if(Dep[x]<Dep[y]) x^=y^=x^=y;
	return y;
}
int main(){
    ...
	dfs(S,0);
	DFS(S,S);
	for(int i=1;i<=M;i++){
		int x,y; scanf("%d%d",&x,&y);
		printf("%d\n",Lca(x,y));
	}
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值