倍增算法之最近公共祖先 (LCA)

 

这里对一些具体的细节不在过多解释,代码中的注释已经比较详细了,这里我们重点解释一下对f数组进行更新时的计算公式是什么意思,f[now][i]=f[f[now][i-1]][i-1],f[now][i]指的是结点now的第2^j级祖先的编号,我们可以将具体的数值带入进行解释,比如将i=1带入,f[now][1]=f[f[now][0]][0]就表示为当前结点的往上找2级的祖先,也就是当前结点的爷爷与当前结点的爸爸的爸爸是等价的。

还有一个要解释的就是在进行倍增跳跃式寻找最近公共祖先的时候为什么会选择这样的倍增方案?我们这时候已经知道当前结点的深度了,因为题目保证有一个根节点,那么当前节点x的depth[x](结点深度)级祖先一定是2个结点的公共祖先,这一点是毋庸置疑的,但我们要找的是最近的公共祖先结点,所以我们要找的节点的级别一定小于等于节点的深度,这里就相当于对节点的深度值进行二进制的转化,从高位到低位进行枚举,至于为什么是从高到低枚举而不是从低到高,原因是从低位开始枚举的话会出现“悔棋”的情况,但从高位就不会,举个例子,拿 5 为例,从小向大跳,5 ≠ 1 + 2 + 4 所以我们还要回溯一步,然后才能得出5 = 4+1 ;而从大向小跳,直接可以得出5 = 4 + 1 。拿二进制为例,5 ( 101 ),从高位向低位填很简单,如果填了这位之后比原数大了,那就不填了,这个过程是很好操作的。

上代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N =1E6+10;
int n,m,s;
int h[N],ne[N],e[N],idx;
int depth[N],f[N][25],lg[N];//depth[i]代表第i个节点的深度,f[i][j]代表结点i的第2^j级结点,lg[i]表示对log(i)向下取整后加1 
void add(int u,int v)//链式前向星 
{
	e[idx]=v;
	ne[idx]=h[u];
	h[u]=idx++;	
} 
void dfs(int now,int father)//对每个节点的深度和各级祖宗节点的信息进行处理 
{
	depth[now]=depth[father]+1;
	f[now][0]=father;//更新当前节点的父亲结点 
	for(int i=1;(1<<i)<=depth[now];i++)
	 f[now][i]=f[f[now][i-1]][i-1];//更新各级祖宗节点的递推式 
	 for(int i=h[now];i!=-1;i=ne[i])//继续向下更新 
	 {
	 	int j=e[i];
	 	if(j!=father)
	 	dfs(j,now);
	 }
}
int lca(int x,int y)
{
	if(depth[x]<depth[y])
	swap(x,y);//始终使x>y,这样我们就只需要计算这一种情况就够了 
	while(depth[x]>depth[y])//先让x跳到于y相同深度的位置 
	x=f[x][lg[depth[x]-depth[y]]-1]; 
	if(x==y)//特判如果这是x和y相等就说明当前的x就是最近公共祖先 
	return x;//这里一定要特判,不然找到的的确是公共祖先,但却不是最近公共祖先 
	for(int i=lg[depth[x]]-1;i>=0;i--)//倍增跳跃找最近公共祖先 
	 if(f[x][i]!=f[y][i])
	 x=f[x][i],y=f[y][i];
	 return f[x][0];
}
int main()
{
	cin>>n>>m>>s;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
	}
	dfs(s,0);//预处理每个结点的深度值和其各级祖宗节点的信息 
	for(int i=1;i<=n;i++)
	lg[i]=lg[i-1]+(1<<lg[i-1]==i);//这个为啥这么算举个例子就知道了,至于为啥lg[i]表示log(i)下取整加一?因为好算呗。。。。 
	for(int i=1;i<=m;i++)
	{
		int a,b;
		cin>>a>>b;
		cout<<lca(a,b)<<endl;
	}
	return 0;
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值