LCA详解

引入

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

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

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

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

倍增

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

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

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

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

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

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

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

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

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

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

倍增LCA

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

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

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

我们先来预处理一下。

根据上述的倍增基本原理,我们需要预处理出每一个节点 x xx 向上 2 i 2^i2i 层的祖先。我们设 f x , i f_{x,i}fx,i 表示节点 x xx2 i 2^i2i 级祖先,因为 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_xfx,0=fax,其中 f a x fa_xfax 表示 x xx 的父亲节点。

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

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

对于 ⌊ log ⁡ 2 x ⌋ \left\lfloor \log_{2}x\right\rfloorlog2x,我们可以使用 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\rfloorlog2i,使用时可以直接调用。

C o d e CodeCode(预处理)

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 xxy yy 跳到同一深度,我们设较深的一个为 x xx,另一个为 y yy,则我们要让 x xx 向上跳,一直跳到与 y yy 同一深度。这个过程我们使用倍增:

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

然后我们要将 x xxy yy 跳到LCA,也就是第二步。和前面相似的,我们使用倍增。但是这里有一个需要注意的点:我们并不知道 x xxy yy 距离它们的LCA有多远,而直接倍增跳LCA的话可能到达非最近公共祖先,所以这个时候我们需要将 x xxy yy 都跳到它们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;
}

版权声明:本文为Wu_while原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。