最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。为了方便,我们记某点集 S = { v 1 , v 2 , … , v n } S=\{v_1,v_2,\ldots,v_n\} S={v1,v2,…,vn}的最近公共祖先为 L C A ( S ) LCA(S) LCA(S)或 L C A ( v 1 , v 2 , . . . , v n ) LCA(v_1,v2,...,v_n) LCA(v1,v2,...,vn)。
除了暴力沿着树根的方向一步步往上以外,常见的求解 LCA 问题的算法有三种,分别是倍增算法、离线tarjan算法、树链剖分算法。它们各有优缺点,需要在不同的时候选择恰当的算法进行使用。
倍增算法,顾名思义就是翻倍。它是一种和二分法思路相反的高效算法。二分法是每次缩小一半,从而以 O ( log n ) O(\log n) O(logn)的速度定位到问题的解。而倍增算法是每次扩大一倍,以 O ( 2 n ) O(2^n) O(2n)的速度逼近问题的解。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。
这个方法在很多算法中均有应用,其中最常用的有两大类:
#include
#include
#include
#include
using namespace std;
typedef int ll;
const ll maxn=500010;
struct node
{
ll v;
ll next;
}e[2*maxn];
ll p[maxn],t=0;
ll n,m,s;
ll parent[maxn][30],d[maxn];
void insert(ll u,ll v)
{
e[t].v=v;
e[t].next=p[u];
p[u]=t++;
}
void dfs(ll u,ll pre)
{
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v;
if(d[v]==-1)
{
parent[v][0]=u;
d[v]=d[u]+1;
dfs(v,u);
}
}
}
ll LCA(ll x,ll y)
{
ll i;
if(d[x]<d[y])
swap(x,y);
for(i=0;(1<<i)<=d[x];i++)
;
i--;
for(ll j=i;j>=0;j--)
if(d[x]-(1<<j)>=d[y])
x=parent[x][j];
if(x==y)
return x;
for(ll j=i;j>=0;j--)
{
if(parent[x][j]!=parent[y][j])
{
x=parent[x][j];
y=parent[y][j];
}
}
return parent[x][0];
}
int main()
{
memset(d,-1,sizeof(d));
memset(p,-1,sizeof(p));
scanf("%d%d%d",&n,&m,&s);
for(ll i=1;i<=n-1;i++)
{
ll u,v;
scanf("%d%d",&u,&v);
insert(u,v);
insert(v,u);
}
d[s]=0;
dfs(s,-1);
for(ll level=1;(1<<level)<=n;level++)
for(ll i=1;i<=n;i++)
parent[i][level]=parent[parent[i][level-1]][level-1];
for(ll i=1;i<=m;i++)
{
ll x,y;
scanf("%d%d",&x,&y);
ll lca=LCA(x,y);
printf("%d\n",lca);
}
return 0;
}
有时候因为题目特性,倍增求 LCA 可能会比较慢。而由于一些强制在线的原因, tarjan 离线的方法求 LCA 的方法也不再适用,这时候就需要下面讲到的树链剖分算法进行求解了。主要思想是利用轻重链的划分来快速在树上进行跳转。
#include
#include
#include
using namespace std;
typedef long long ll;
const ll maxn=500010;
struct node
{
ll v;
ll next;
}e[maxn*2];
ll p[maxn],t=0;
ll d[maxn],size[maxn],son[maxn],top[maxn],fa[maxn];
ll n,m,S;
void insert(ll u,ll v)
{
e[t].v=v;
e[t].next=p[u];
p[u]=t++;
}
void dfs1(ll u)
{
size[u]=1;
for(ll i=p[u];i!=-1;i=e[i].next)
{
ll v=e[i].v;
if(fa[u]!=v)
{
fa[v]=u;
d[v]=d[u]+1;
dfs1(v);
size[u]+=size[v];
if(size[son[u]]<size[v])//son[u]表示结点u的儿子中以其为根的子树size最大的那个(用来构造重链)
son[u]=v;
}
}
}
void dfs2(ll u)
{
if(u==son[fa[u]])//如果u是fa[u]的儿子中拥有最大size的那个
top[u]=top[fa[u]];//把top[u]连向top[fa[u]](形成重链)
else
top[u]=u;//否则指向它自己
for(ll i=p[u];i!=-1;i=e[i].next)
if(e[i].v!=fa[u])
dfs2(e[i].v);
}
ll LCA(ll x,ll y)
{
while(top[x]!=top[y])//x,y不在同一条重链上时
{
if(d[top[x]]>d[top[y]])//将深度大的上提
x=fa[top[x]];
else
y=fa[top[y]];
}
if(d[x]<d[y])//返回x,y中在较上方的那个
return x;
return y;
}
int main()
{
memset(p,-1,sizeof(p));
scanf("%lld%lld%lld",&n,&m,&S);
for(ll i=1;i<n;i++)
{
ll u,v;
scanf("%lld%lld",&u,&v);
insert(u,v);
insert(v,u);
}
d[S]=1;
dfs1(S);//预处理d和fa
dfs2(S);//预处理top
for(ll i=1;i<=m;i++)
{
ll u,v;
scanf("%lld%lld",&u,&v);
printf("%lld\n",LCA(u,v));
}
return 0;
}
P3379 【模板】最近公共祖先(LCA)
P2420 让我们异或吧
P5836 [USACO19DEC]Milk Visits S