LCA算法以及原理详解

本文详细介绍了最近公共祖先(LCA)算法,包括暴力求解、倍增算法、ST算法和Tarjan算法。倍增算法通过跳跃2^j步寻找LCA,预处理时间复杂度为O(nlogn),查询时间为O(logn)。ST算法利用中序遍历特性,借助ST或线段树求解LCA,而Tarjan算法利用DFS后序遍历和并查集处理LCA问题。
摘要由CSDN通过智能技术生成

LCA-最近公共祖先

  LCA(Least Common Ancestors),即最近公共祖先,这种描述是基于树结构的,也即我们通通常只在树结构中考虑祖先问题。树实际上就是图论中的有向无环图,而要研究LCA问题,首先我们要指定树中的一个顶点为根节点,并以该节点遍历有向无环图,生成一颗DFS序下的树,假设我们要查询的两个节点为u,v,DFS序下根节点到两点的最短路径分别是(r,u),和(r,v),LCA就是(r,u)与(r,v)公共路径的最后一个节点,如下图所示,w即为LCA。
在这里插入图片描述
  换句话说,u,v的LCA就是以r为根的树中,u到v的最短路径中深度最小的点(假设根节点的深度为1,而深度是往下递增的)。

暴力

  暴力法求解一对节点的LCA时时间复杂度是O(n)的,所以当查询多对节点的LCA时,暴力算法的时间复杂度往往不满足要求。
  暴力法就是通过不断地将深度较深的点往上求父节点,直到两个点的父节点重合时,即可得到LCA。下面给出一种实现方法。

int dfs1(int k,int u,int v){
   //暴力法1的dfs方法,u,v为要查询的两个节点 
	int count=0; 
	int ans=-1;
	for(int i=0;i<g[k].size();i++){
   
		if(!color[g[k][i]]){
   
			color[g[k][i]]=1;
			int num=dfs1(g[k][i],u,v);
			if(num>=0){
   
				count++;
				ans=num;
			} 
		}
	}
	//如果有两个分支返回返回的是节点编号,那么此时该节点一定是LCA,返回节点编号 
	if(count==2)ans=k;
	//遍历到u,或者v时,返回该点编号即可 
	if(k==u||k==v)ans=k;
	//否则返回-1,也即不返回任何节点编号 
	return ans;
} 
int violence1(int u,int v){
   
	memset(color,0,sizeof(color));
	return dfs1(0,u,v);//假设根节点为0 
} 

倍增算法

  倍增法其实就是每一步尽可能跳的多一点,他的思想与二分的想法其实是一致的,假设我们要求解LCA(u,v),暴力的想法是我们始终将深度较深的往上跳跃一步,直到u,v的深度第一次相等时,此时该节点就是LCA(u,v),但是这样做的话,在一个接近线性的树中,时间复杂度是O(n)的,当有多组查询时,这种开销就无法承受了。
  倍增的原理是每一次尽可能地多跳一些步数,而不是一步一步往上跳,当时如何快速找到应该跳的步数呢?考虑到depth[LCA(u,v)]-depth[u]是一个深度差值,而一开始我们是不知道LCA(u,v)到底是哪一个节点,并且u和v也可能不在同一深度,那么为了便于计算,我们应该首先间u和v调整到同一深度。
  当u和v的深度相同时,此时depth[LCA(u,v)]-depth[u]=depth[LCA(u,v)]-depth[v],也即u和v跳相同的步数即可到达最近公共祖先。由于此时不知道LCA(u,v)到底是哪一个,所以此时跳跃的步数只能使尝试性的,否则我们可能会直接跳过头,那么什么样的尝试序列是比较好的呢?考虑到搜索算法的时间复杂度上限,我们可以选择跳跃2^j步进行尝试。
  但是这里面存在一个问题就是,当u和v跳跃2^j步后,两者的祖先相等,此时该节点就是u和v的一个公共祖先,但是却不一定是最近公共祖先,所以为了避免这种情况的出现,我们选择u和v跳跃2 ^j步后,两者的祖先不是公共祖先的最大j,进行跳跃;跳跃2 ^j步后,此时u,v依然在同一深度,与之前的子问题是等价的,我们可以继续该操作,直到j==0跳出循环;由于我们每一步都不会直接跳到最近公共祖先,所以最后得到的结果中,u和v都跳跃到了最近公共祖先的下一层,此时我们直接返回u或者v的父节点即可。
  上述跳跃2 ^j的操作是通过一个dp数组来实现的,我们用dp[i][j]表示节点i的第2 ^j个祖先,那么dp[i][j]可以由更小的子问题推导出,显然我们可以得到dp[i][j]=dp[dp[i][j-1]][j-1];这样我们在遍历到每一个节点是预处理出以该节点的所有可能的dp值即可。初始化时dp[i][0]其实就是i的父节点,我们可以直接在遍历的时候赋值。
  下图是该算法每一次尝试跳跃时所做的选择。
在这里插入图片描述

例题

LCA模板题

#include<bits/stdc++.h>
using namespace std;

//LCA算法复习
//动态规划+倍增优化

const int N=1000010;
int edge[N];
int nest[N];
int last[500010];
int cnt=1;

inline void add(int u,int v){
   
	nest[cnt]=last[u];
	edge[cnt]=v;
	last[u]=cnt;
	cnt++;
	return;
}
//用来保存父节点 
int dp[500010][20];
//保存深度 
int depth[500010]; 
//预处理出父亲切点 
bool vise[500010];
void DFS(int k){
   
	//预处理出DP数组
	for(int i=1;(1<<i)<depth[k];i++){
   
		dp[k][i]=dp[dp[k][i-1]][i-1];
	}
	for(int i=last[k];i;i=nest[i]){
   
		//求解直接公共祖先;
		if(vise[edge[i]])continue;
		vise[edge[i]]=true; 
		depth[edge[i]]=depth[k]+1;
		dp[edge[i]][0]=k;
		DFS(edge[i]);
	}
	return; 
} 
int lca(int u,int v){
   
	if(depth[u]<depth[v])swap(u,v);
	//弹节点 
	int k=log2(depth[u]-depth[v]);
	for(int i=k;i>=0;i--){
   
		if(depth[dp[u][i]]>=depth[v])u=dp[u][i];
	} 
	if(u==v)return u;
	//查询
	k=log2(depth[u]);
	for(int i=k;i>=0;i--){
   
		if(dp[u][i]==dp[v][i])continue;
		u=dp[u][i];
		v=dp[v][i];
	} 
	return dp[u][0];
}
int main(){
   
	//LCA模板题
	int n,m,s;
	cin>>n>>m>>s;
	int u,v;
	for(int i=0;i<n-1;i++){
   
		scanf("%d %d",&u,&v);
		add(u,v);
		add(v,u);
	} 
	//这里要初始化为1,避免与深度为0的0产生歧义。 
	depth[s]=1;
	vise[s]=true;
	DFS(s);
	for(int i=0;i<m;i
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值