最近公共祖先(LCA)及其倍增算法实现

最近公共祖先(LCA)

今天看看最近公共祖先(LCA),也就是所谓的最小公共祖先。
我们首先了解一下什么是LCA,我们通过几棵树来理解一下吧。
图一
如图所示,这棵树是以1为根节点的一棵树,我们举一个例子,3和5的LCA就是2,4和5的LCA就是1,3和2的LCA就是2本身。是不是有点明白?
接下来,我们不改变节点间的关系,只改变根节点。
图二
如图所示,我们把2作为根节点,那么这棵二叉树俨然就变成了多叉树,然后我们再举几个例子,比如说,3和5的LCA仍然是2,但4和5的LCA就变成了2,所以我们发现,根节点选取的改变,也会导致两点之间LCA的数值的变化。
理清概念之后我们就上一个模板题来看看,这个题的样例可以自己出,在这里不再给大家提供样例,话说可以去luogu过一下嘞。

题目描述
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
输入格式:
第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。
(数据保证可以构成树)。
接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。
输出格式:
输出包含M行,每行包含一个正整数,依次为每一个询问的结果。 时空限制:1000ms,128M

学过最小生成树(MST)的人都应该知道,像这样的树,它的边数为N-1条。所以就算是题目中不说,我们也认为它是N-1条。
接下来我们就一步一步的解释:

在这里我们先提前引入一个数组,稍后讲倍增的时候会解释。fa[i][j]的含义就是,对于一个节点i,它的第2^j个父亲节点的编号,比如说上面第一个图中fa[5][0]=2,fa[5][1]=1。

做好图论题,比较基础且重要的就是建图,利用邻接表,从根节点出发,用DFS来建表。与其它题不同的一点就是这里每次son的建边都需要对它的fa[son][]数组进行更新,fa[son][i+1]=fa[fa[son][i]][i]。理解这一点很重要,这里就已经有倍增算法的思想了,可以自己任意画一棵树来试一试,是很奇妙的。

然后求LCA。

对于两个点X和Y,一个比较自然的思路就是,既然要找他们的LCA,就一个一个的顶上去就行呗,等到他们走过的路径有节点的时候就输出。这样做确实能得到正确的答案,但是会超时。

我们的思路是,首先保证deep[X]>=deep[Y],这保证了我们在操作的时候只操作X(当然只操作Y也是可以的)。然后让X向上搜,直至其深度等于Y的深度,这里通过倍增算法让其快速上搜,稍后我们解释倍增算法。等到二者深度相同的时候,就让他们同时上溯至同一节点,此节点即为X与Y节点的LCA。

先把自己会的搞上去以便于理解。
建边操作(话说我前面写的几个博客的建边操作太鬼畜了有木有),用3个数组来存边的属性,就是邻接表。

void add(int xx,int yy)
{
    nxt[++temp]=hd[xx];
    y[temp]=yy;
    hd[xx]=temp;
}

然后是建图,上面也已经说了,需要注意的就是同一个节点不能被经历两次,所以用一个bool数组来记忆。

void dfs(int x)
{
    int p,son;
    p=hd[x];
    while(p)
    {
        son=y[p];
        if(!b[son])
        {
            b[son]=1;
            fa[son][0]=x;
            deep[son]=deep[x]+1;
            int ii=0,po=x;
            while(fa[po][ii]!=0)
            {
                fa[son][ii+1]=fa[po][ii];
                po=fa[po][ii];
                ii++;
            }
            dfs(son);
        }
        p=nxt[p];
    }
}

接下来重点说倍增算法在LCA中的应用(详见注释)。

int lca(int xx,int yy)
{
    int i,j;
    if(deep[xx]<deep[yy])swap(xx,yy);
    for(i=0; (1<<i)<=deep[xx]; i++);
    i--;
//找当前点到树根距离大小的2^j距离中,j的最大值。比如说:deep[xx]=3时,jmax=2,deep[xx]=9时,jmax=3。
    for(j=i; j>=0; j--)
        if(deep[xx]-(1<<j)>=deep[yy])
            xx=fa[xx][j];
    if(xx==yy)return xx;
//当xx与yy同深度时,恰好就在yy这个位置上,那么就直接xx或者yy是LCA
    for(j=i; j>=0; j--)
    {
        if(fa[xx][j]!=fa[yy][j])
//这里的IF语句功能比较多,一方面可以筛掉超过当前点到树根距离大小的2^j距离,还可以在二者相同时不操作它,因为已经达成找到LCA的小目标了。
        {
            xx=fa[xx][j];
            yy=fa[yy][j];
        }
    }
    return fa[xx][0];
}

这里LCA讲的可能比较糙,思路可以看看这个博客:
http://www.cnblogs.com/yyf0309/p/5972701.html
我是从这里学会的LCA的倍增实现的:
http://blog.csdn.net/wangwangbu/article/details/51453084
最后,有任何疑问欢迎评论区提出!

完整代码 C++

(感谢HQG_AC同学的建议!)

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
int temp,i,j,m,n,s;
int hd[500001],deep[500001],fa[500001][30];

struct data
{
    int v,nxt;
}a[1000001];

int r()
{
    int ans=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-')
        f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        ans*=10;
        ans+=ch-'0';
        ch=getchar();
    }
    return ans*f;
}

void add(int x,int y)
{
    a[++temp].v=y;
    a[temp].nxt=hd[x];
    hd[x]=temp;
}

void dfs(int x)
{
    int p,son;
    for(p=hd[x];p;p=a[p].nxt)
    {
        son=a[p].v;
        if(!deep[son])
        {
            deep[son]=deep[x]+1;
            fa[son][0]=x;
            int ii=0,po=x;
            while(fa[po][ii])
            {
                fa[son][ii+1]=fa[po][ii];
                po=fa[po][ii];
                ii++;
            }
            dfs(son);
        }
    }
}

int lca(int x,int y)
{
    if(deep[x]<deep[y]) swap(x,y);
    int lim=log2(deep[x])+1;
    for(i=lim;i>=0;i--)
    {
        if(deep[fa[x][i]]>=deep[y])
        x=fa[x][i];
    }
    if(x==y) return y;
    for(i=lim;i>=0;i--)
    {
        if(fa[x][i]!=fa[y][i])
        x=fa[x][i],y=fa[y][i];
    }
    return fa[x][0];
}

int main()
{
    n=r(),m=r(),s=r();
    int x,y;
    for(i=1;i<n;i++)
    {
        x=r(),y=r();
        add(x,y);
        add(y,x);
    }

    deep[s]=1;
    dfs(s);

    for(int ii=1;ii<=m;ii++)
    {
        x=r(),y=r();
        printf("%d\n",lca(x,y));

    }

    return 0;
}

这里写图片描述

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值