相比于暴力算法倍增思想的LCA明显给省时间和空间
原理和同样是使用倍增思想的RMQ-ST 算法类似,比较简单,想清楚后很容易实现。
对于每个节点u , fa[u][k] 表示 u 的第2k个祖先是谁。很容易就想到递推式: fa[j][i] = fa[fa[j][i - 1]][i - 1]; 根据二进 制原理,理论上 u 的所有祖先都可以根据fa数组多次跳转得到,这样就间接地记录了每个节点的祖先信息。
查询LCA(u,v)的时候:
(一)u和v所在的树的层数如果一样,令u'=u。否则需要平衡操作(假设u更深),先找到u的一个祖先u', 使得u'的层数和v一样,此时LCA(u,v)=LCA(u',v) 。证明很简单:如果LCA(u,v)=v , 那么u'一定等于v ;如果LCA(u,v)=k ,k!=v ,那么k 的深度一定小于 v , u、u'、v 一定在k的子树中;综上所述,LCA(u,v)=LCA(u',v)一定成立。
(二)此时u' 和 v 的祖先序列中一开始的部分一定有所重叠,重叠部分的最后一个元素(也就是深度最深,与u'、v最近的元素)就是所求的LCA(u,v)。这里fa数组就可以派上用场了。找到第一个不重叠的节点k,LCA(u,v)=fa[k][0] 。 找k的过程利用二进制贪心思想,先尽可能跳到最上层的祖先,如果两祖先相等,说明完全可以跳小点,跳的距离除2,这样一步步跳下去一定可以找到k。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
int head[500010];
int fa[500010][20]={0};
int deep[500010]={0},cnt=0; //deep保存每个节点的深度,CNT为保存图的虚拟指针
int n,m,s; // s为根节点编号
struct kk{
int next,v;
}e[1000010]; //保存边 无向图开成两倍哦
void add(int a,int b)
{
cnt++;
e[cnt].v=b;
e[cnt].next=head[a];
head[a]=cnt;
} //加入边 so easy
void dfs(int x)
{
for(int i=head[x];i;i=e[i].next)
{
int v=e[i].v;
if(!deep[v])
{
deep[v]=deep[x]+1;fa[v][0]=x;
dfs(v);
}
}
} //dfs一次就可以保存下所有点的深度
int lca(int x,int y)
{
if(deep[x]<deep[y])swap(x,y); // 如果深度不同 调到同一深度
for(int i=19;i>=0;i--) //19为可能的最大深度
{
if(deep[fa[x][i]]>=deep[y])
x=fa[x][i];
}
if(x==y)return x;
for(int i=19;i>=0;i--)
if(fa[x][i]!=fa[y][i]) //同时向上跳
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<n;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);add(b,a);
}
deep[s]=1;
dfs(s);
for(int i=1;i<=19;i++)
for(int j=1;j<=n;j++)
fa[j][i]=fa[fa[j][i-1]][i-1];
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}