欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

求LCA——最近公共祖先 倍增算法

程序员文章站 2022-04-11 10:24:22
...

LCA是啥呢,LCA就是一棵树里两个节点的最近公共祖先,如下图

求LCA——最近公共祖先 倍增算法

2号节点和3号节点的LCA就是1, 5号节点和11号节点的LCA就是2,8号节点和4号节点的lca就是4

那么怎么求LCA呢。首先要建树,然后最容易想到的就是两个节点一起向上跳,第一个相遇的节点就是LCA

输入输出格式可参考洛谷P3379 LCA模板题

输入格式:

第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。

接下来N-1行每行包含两个正整数x、y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树)。

接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。

输出格式:

输出包含M行,每行包含一个正整数,依次为每一个询问的结果。

 

先用邻接表存路径。

void add(int x,int y)
{
    to[++cnt]=y;
    nxt[cnt]=head[x];
    head[x]=cnt;
}

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

存路径后,我们用dfs建树,把每个节点的深度记录下来

void dfs(int x,int fa)//fa表示父节点编号
{
    int i;
    f[x]=fa;//f[x]表示x节点的父节点
    d[x]=d[fa]+1;
    for(i=head[x];i;i=nxt[i])
        if(to[i]!=fa)
            dfs(to[i],x);
}

其次需要先把两个节点跳到同一深度,然后一起往上跳即可

int work(int x,int y)
{
    int i;
    if(d[x]<d[y])
        swap(x,y);
    while(d[x]>d[y])
    	x=f[x];//移动至同一深度
    while(x!=y)
    	x=f[x],y=f[y];
    return x;
}

以上就是暴力求LCA,以为能AC吗??妥妥的TLE!!!

那么为什么T呢?是因为一次只移动到父亲节点要跳好多次,这样太慢了!

那么怎么才能快点呢?就要用倍增了!设一个grd[x][i]数组表示x节点想上跳2的i次方后的节点。所以i<=log2(n)

这个数组怎么用呢?令d[x]>=d[y]。首先还是要跳到同一深度中,先让x跳2^20次(最大)。假设我们发现深度比y小了,这就表示跳过了,我们再试试跳2^19次,如果还过了,那就再变小。如果发现深度还没到,那就跳呗,接着我们再跳,就该试试跳2^18次了,为什么不能跳两个2^19次呢?这是以为两个2^19次就是2^20次啊,肯定可以不用跳两次相同的。这样跳就能大大减少时间了。

到同一深度后,就是两个点一起往上跳了。我们还是让两个点跳2^20次,如果发现跳完后相同了,就可以确定跳到祖先了,但不一定是最近的,所以我们先不着急跳,接着,试试跳2^19次,如果还是相同,那就再变小,如果不相同,那就跳(跟上面差不多),最后跳完,可以确定x和y再向上跳一次就是LCA了,因为我们跳的时候判定了如果跳之后相同那就不跳。但是注意,当y是x的祖先时,跳到同一深度时x==y已经是LCA了,此时就不用再往上跳了。

举个栗子,还是刚才那张图,我们模拟一下求11和7的LCA                                                                                                               

求LCA——最近公共祖先 倍增算法

首先移动到同一深度,d[7]=3,d[11]=4,所以我们从2^20次(最大)开始,一直到2^1都不行,所以节点11跳了2^0次跳到了节点6。

接着两个节点一块儿跳,从2^20次,最后还是只能跳2^0次,变成了节点2和节点3,最终再跳一次即节点1就是LCA。

em……这个栗子有点小。。。。。。。

好了,那怎么预处理grd数组呢?我们可以在dfs建树的时候预处理,那么我们可以确定grd[x][0]就是他的爸爸,那么怎么确定grd[x][1]呢?grd[x][1]不就是爸爸的爸爸(爷爷)嘛,所以grd[x][1]=grd [ grd [ x ] [ 0 ] ] [ 0 ](为了让大家看清楚特意用空格隔开嘿嘿嘿),那么grd[x][2]呢(注意这是爷爷的爷爷,而不是爷爷的爸爸)?爷爷的爷爷不就等于grd [ grd [ x ] [ 1 ] ] [ 1 ]嘛。

以此类推,我们得出grd[x][i]=grd[ grd [ x ] [ i - 1 ] ] [ i - 1 ]。是因为2^(i-1)+2(i-1)=2^i,即往上跳两次2^(i-1)就是往上跳2^i

好了我们再捋一遍思路,邻接表+预处理grd数组+跳到相同深度+一起往上跳,代码如下。

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>

using namespace std;

inline int read()
{
    int f=1,x=0;
    char c=getchar();
    while(!isdigit(c)){if(c=='-')f*=-1;c=getchar();}
    while(isdigit(c)){x=x*10+c-'0';c=getchar();}
    return x*f;
}

const int N=5e5+5;
int a[N],n,m,s,to[N<<1],head[N],nxt[N<<1],grd[N][23],d[N],cnt;

void add(int x,int y)
{
    to[++cnt]=y;
    nxt[cnt]=head[x];
    head[x]=cnt;//邻接表
}

void dfs(int x,int fa)
{
    int i;
    grd[x][0]=fa;//父节点
    d[x]=d[fa]+1;
    for(i=1;i<21;i++)//其实到本题19就可以了,习惯到20整一点哈哈哈
        grd[x][i]=grd[grd[x][i-1]][i-1];//预处理grd数组
    for(i=head[x];i;i=nxt[i])
        if(to[i]!=fa)
            dfs(to[i],x);
}

int lca(int x,int y)
{
    int i;
    if(d[x]<d[y])
        swap(x,y);
    for(i=20;i>=0;i--)//从大到小枚举
        if((1<<i)<=d[x]-d[y])//1<<i就是2^i,也可以预处理成数组
            x=grd[x][i];
    if(x==y)
        return x;//当y是x的祖先时
    for(i=20;i>=0;i--)
        if(grd[x][i]!=grd[y][i])
            x=grd[x][i],y=grd[y][i];//一起往上跳
    return grd[x][0];//返回父节点
}

int main()
{
    n=read();m=read();s=read();
    int i,j,x,y;
    for(i=1;i<n;i++)
    {
        x=read();y=read();
        add(x,y);
        add(y,x);//邻接表
    }
    dfs(s,0);//预处理
    for(i=1;i<=m;i++)
    {
        x=read();y=read();
        printf("%d\n",lca(x,y));
    }
    return 0;
}

当然,遇到相应的题可再加数组,比如有时可定义dis[x][i]表示x节点向上跳2^i的距离(权),或者表示最大最小值等。

相关标签: LCA