LCA详解

本文详细介绍了如何运用倍增算法解决多叉树中两个节点的最近公共祖先问题。通过预处理每个节点的2的幂次幂祖先,并在DFS过程中计算节点深度,结合倍增策略,可以高效地找到最近公共祖先。代码示例展示了预处理和求解LCA的具体实现。
摘要由CSDN通过智能技术生成

引入

来看例题:洛谷 P3379 【模板】最近公共祖先(LCA)

给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

首先,我们可以暴力来做,把 x x x 的祖先标记一下,然后去找 y y y 的祖先,找到的第一个被标记过的就是最近公共祖先。

显然,这样做是超时的,所以我们就要用奇妙的算法:倍增来解决了。

倍增

或许你可以通过小白兔的故事来了解一下倍增,这里只阐述倍增的基本原理。

那么倍增的原理是什么呢?

首先,你需要知道这样一个定理:任何一个数都可以分解为若干个互不相同的 2 2 2 的幂之和。这是因为十进制可以转化为二进制,二进制的第 n n n 位可以看做是 2 n − 1 2^{n-1} 2n1,而二进制的每一位最大为 1 1 1,所以互不相同。

假设我们要去距离为 13 13 13 的一个地方,我们从 8 8 8 开始计算:

向前走 8 8 8 个单位,没有超过目的地,走;

向前走 4 4 4 个单位,没有超过目的地,走;

向前走 2 2 2 个单位,超过了目的地,不走;

向前走 1 1 1 个单位,到达目的地,走。

显然,我们是从 2 3 2^3 23 开始依次尝试,没超过目的地便走,超过了便不走,一直尝试到 2 0 2^0 20

现在的问题是,我们怎么确定从哪个数开始尝试。显然,我们应该从离 13 13 13 最近的且比 13 13 13 小的 2 2 2 的幂开始,因为再小会导致无法到达,大了会有浪费。

倍增LCA

x x x y y y 的LCA的步骤大致分为两个部分:

  1. x x x y y y 跳到树的同一层(即深度相同)
  2. 一起向上跳,一直跳到最近公共祖先

这两步都可以使用倍增优化。

我们先来预处理一下。

根据上述的倍增基本原理,我们需要预处理出每一个节点 x x x 向上 2 i 2^i 2i 层的祖先。我们设 f x , i f_{x,i} fx,i 表示节点 x x x 2 i 2^i 2i 级祖先,因为 2 i = 2 i − 1 + 2 i − 1 2^i=2^{i-1} + 2^{i-1} 2i=2i1+2i1,所以有状态转移方程:

f x , i = f ( f x , i − 1 ) , i − 1 f_{x,i}=f_{(f_{x,i-1}),i-1} fx,i=f(fx,i1),i1

边界条件为 f x , 0 = f a x f_{x,0}=fa_x fx,0=fax,其中 f a x fa_x fax 表示 x x x 的父亲节点。

我们可以在对树的DFS中完成它,同时处理出节点的深度 d e p x dep_x depx

还有一个问题,我们要知道倍增是从大向小处理的,所以我们需要得到开始的 i i i 值。由上述倍增基本原理得, i i i 应该使 2 i 2^i 2i x x x 最近且比 x x x 小的。由此可以得到, i = ⌊ log ⁡ 2 x ⌋ i=\left\lfloor \log_{2}x\right\rfloor i=log2x

对于 ⌊ log ⁡ 2 x ⌋ \left\lfloor \log_{2}x\right\rfloor log2x,我们可以使用 cmath \text{cmath} cmath 库自带的函数 log2(),但是这样做的常数较大,所以我们使用这样一个常数优化:

lg[1]=0;
for(int i=2;i<=n;i++)
	lg[i]=lg[i/2]+1;

其中 lg[i] 就等于 ⌊ log ⁡ 2 i ⌋ \left\lfloor \log_{2}{i}\right\rfloor log2i,使用时可以直接调用。

C o d e Code Code(预处理)

void dfs(int x,int fa)
{
	f[x][0]=fa;
	dep[x]=dep[fa]+1;
	for(int i=1;i<=lg[dep[x]];i++)
		f[x][i]=f[f[x][i-1]][i-1];
	for(int i=head[x];~i;i=edge[i].nxt)
	{
		if(edge[i].to==fa)
			continue;
		dfs(edge[i].to,x);
	}
}

预处理结束,我们就可以跑LCA了。先来看第一步。

我们要将 x x x y y y 跳到同一深度,我们设较深的一个为 x x x,另一个为 y y y,则我们要让 x x x 向上跳,一直跳到与 y y y 同一深度。这个过程我们使用倍增:

if(dep[x]<dep[y])
	swap(x,y);
while(dep[x]>dep[y])
	x=f[x][lg[dep[x]-dep[y]]];

然后我们要将 x x x y y y 跳到LCA,也就是第二步。和前面相似的,我们使用倍增。但是这里有一个需要注意的点:我们并不知道 x x x y y y 距离它们的LCA有多远,而直接倍增跳LCA的话可能到达非最近公共祖先,所以这个时候我们需要将 x x x y y y 都跳到它们LCA的前一个节点,然后返回父节点:

for(int i=lg[dep[x]];i>=0;i--)
	if(f[x][i]!=f[y][i])
		x=f[x][i],y=f[y][i];
return f[x][0];

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#define MAXN 500010
#define MAXM 500010
using namespace std;
struct Edge
{
	int to;
	int nxt;
}
edge[MAXM*2];
int head[MAXN],size;
void add(int from,int to)
{
	edge[++size].nxt=head[from];
	edge[size].to=to;
	head[from]=size;
}
void init()
{
	memset(edge,-1,sizeof(edge));
	memset(head,-1,sizeof(head));
}
int n,m,s;
int u,v;
int dep[MAXN],f[MAXN][30];
int lg[MAXN];
void dfs(int x,int fa)
{
	f[x][0]=fa;
	dep[x]=dep[fa]+1;
	for(int i=1;i<=lg[dep[x]];i++)
		f[x][i]=f[f[x][i-1]][i-1];
	for(int i=head[x];~i;i=edge[i].nxt)
	{
		if(edge[i].to==fa)
			continue;
		dfs(edge[i].to,x);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y])
		swap(x,y);
	while(dep[x]>dep[y])
		x=f[x][lg[dep[x]-dep[y]]];
	if(x==y)
		return x;
	for(int i=lg[dep[x]];i>=0;i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	return f[x][0];
}
int main()
{
	init();
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<=n-1;i++)
	{
		scanf("%d%d",&u,&v);
		add(u,v);
		add(v,u);
	}
	for(int i=2;i<=n;i++)
		lg[i]=lg[i/2]+1;
	dfs(s,0);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&u,&v);
		printf("%d\n",lca(u,v));
	}
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值