引入
给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
首先,我们可以暴力来做,把 x x x 的祖先标记一下,然后去找 y y y 的祖先,找到的第一个被标记过的就是最近公共祖先。
显然,这样做是超时的,所以我们就要用奇妙的算法:倍增来解决了。
倍增
或许你可以通过小白兔的故事来了解一下倍增,这里只阐述倍增的基本原理。
那么倍增的原理是什么呢?
首先,你需要知道这样一个定理:任何一个数都可以分解为若干个互不相同的 2 2 2 的幂之和。这是因为十进制可以转化为二进制,二进制的第 n n n 位可以看做是 2 n − 1 2^{n-1} 2n−1,而二进制的每一位最大为 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的步骤大致分为两个部分:
- 将 x x x 和 y y y 跳到树的同一层(即深度相同)
- 一起向上跳,一直跳到最近公共祖先
这两步都可以使用倍增优化。
我们先来预处理一下。
根据上述的倍增基本原理,我们需要预处理出每一个节点 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=2i−1+2i−1,所以有状态转移方程:
f x , i = f ( f x , i − 1 ) , i − 1 f_{x,i}=f_{(f_{x,i-1}),i-1} fx,i=f(fx,i−1),i−1
边界条件为 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;
}