引入
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;
}