学习笔记:LCA最近公共祖先

引入

LCA即最近公共祖先,是指在有根树中2个点的最近的公共祖先。那么我们该如何求解?
暴力:
在一棵树中,每个点有一个深度,那我们先把一个较深的点跳到和另一个点深度相同的位置,然后一起跳,跳到它们在同一位置时,即为LCA。但是这种方法显然不优美,如果在一条链的2端,那么复杂度将会是O(n)。
那么问题出在哪里呢?很明显一步一步的一起跳显然太慢了,如果我们能一次多跳几步,那么可以降下来了。

原理

基于倍增的思想,我们一次多跳几步。那该跳多少步?
我们知道任意一个整数可以分为2的整数幂的和。
因为每一个偶数肯定可以拆成很多2的和,每个奇数可以分为偶数+1即20
那么基于这样,我们就可以每次跳2k步。
先调整x,y的深度,若x深度大于y,如果相反,那就swap(x,y),那就调整x跳到y的深度,如果跳到同样深度后发现x=y,即在一条链上,那么LCA=y
若x!=y,那么就一起跳,跳到最后,那么x,y相遇只差一步了,它们的父节点即是LCA。

代码实现

我们用dep表示深度,f[x][k]表示x的2k的祖先,因为2k=2k-1+2k-1那么可以递归得到
f[x][k]=f[f[x][k-1]][k-1],那么这样我们可以求出每个点的2k的祖先了,预处理时间复杂度n(n*logn),之后我们就可以以n(logn)的复杂度查询LCA。
所以,这种方法适用于多次求LCA,若只求一次,那不如暴力。
每次查询,表示x的2k即为x=f[x][k]。

预处理:

void init(int x,int far)
{
    
    
	dep[x]=dep[far]+1;
	for(int i=0;i<=21;i++)
	{
    
    
		f[x][i+1]=f[f[x][i]][i];
	}
	for(int i=first[x];i;i=nex[i])
	{
    
    
		int y=to[i];
		if(y==far) continue;
		f[y][0]=x;
		init(y,x);
	}
}

查找:

int lca(int x,int y)
{
    
    
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=21;i>=0;i--)
	{
    
    
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
		if(x==y) return x;
	}
	for(int i=21;i>=0;i--)
	{
    
    
		if(f[x][i]!=f[y][i])
		{
    
    
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}

PS:循环21是代表了n的最大范围的以2为底的对数,因为最多即一条链上要跳这么多次。

应用

题目大意:有2个人分别在不同的城市,有很多城市构成了一棵树,他们2个想要在他们所在城市的中点见面,求出在哪里,若中点是边,则输出边的两个端点。

思路: 既然是中点,我们肯定求x到y的距离,那么距离该如何求呢?
暴力: 我们可以直接DFS x到y,但是O(n)不够优美。
LCA: 深度代表了根节点到每个点的距离,那么我们可否用上这个呢?就可以避免了再求一次。
x到y的路径必定包含了LCA,我们可以发现根节点到x的路径和到y的路径,在LCA处就分叉了,但是其他地方是完全重复的,所以我们可以得到x到y的距离=根到x的距离和根到y的距离-根到LCA的距离*2(计算了2遍),所以我们就可以优美地求出了。

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

const int N=1e4+5,M=2e4+5;
int n,m,x,y;
int first[N],nex[M],to[M],tot;
int f[N][15],dep[N];

inline void add(int x,int y)
{
    
    
	nex[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
}

inline void init(int x,int fa)
{
    
    
	dep[x]=dep[fa]+1;
	for(int i=0;i<=13;i++)
		f[x][i+1]=f[f[x][i]][i];
	for(int i=first[x];i;i=nex[i])
	{
    
    
		int y=to[i];
		if(y==fa) continue;
		f[y][0]=x; //y的父节点是x 
		init(y,x);
	}
}

inline int up(int x,int l)
{
    
    
	for(int i=14;i>=0;i--)
		if(l>=(1<<i)) //每次跳距离 
		{
    
    
			l-=1<<i;
			x=f[x][i];
		}
	return x;
}

inline int lca(int x,int y)
{
    
    
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=14;i>=0;i--)
	{
    
    
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
		if(x==y) return x;//x=y,return x或y不影响
	} 
	
	for(int i=14;i>=0;i--)
	{
    
    
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}

inline int dis(int x,int y,int p)
{
    
    
	return dep[x]+dep[y]-2*dep[p];
}

inline void query(int x,int y)
{
    
    
	int p=lca(x,y);
	int len=dis(x,y,p);
	int mid=len>>1;
	int nx,ny;
	if(dep[x]>dep[y])//中点在x到lca的路径上 
	{
    
    
		nx=up(x,mid);
		if(len%2==0) //len是偶数
			cout<<nx<<endl;
		else
			cout<<nx<<" "<<f[nx][0]<<endl;
	}
	else//相反 
	{
    
    
		ny=up(y,mid);
		if(len%2==0)
			cout<<ny<<endl;
		else
			cout<<f[ny][0]<<" "<<ny<<endl;
	}
}

int main()
{
    
    
	scanf("%d",&n);
	int x,y;
	for(int i=1;i<n;i++)//是一棵树边只有n-1 
	{
    
    
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	init(1,0);//1是根,0父节点 
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
    
    
		scanf("%d%d",&x,&y);
		query(x,y);
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/pigonered/article/details/120872897