LCA在线算法

LCA(Least Common Ancestors),即最近公共祖先。对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
求LCA的算法有很多种,分为在线和离线。离线算法一般有tarjan,在线算法则是树上倍增与rmq。

这里主要讲下在线算法吧:-)

我是一棵树

经过“肉眼扫描算法”,我们可以很快的得出4和6的最近公共祖先是1。

倍增

对于两个同一层(也就是深度一样)的结点,向上寻找他们的最近公共祖先。对于上图的4和6来说,我们首先判断 4 的父亲 6 的父亲是不是同一个结点,如果不是,则继续向上重复 “判断父亲是不是相同的” 这个操作。直到找到为止。
显然,这么一层层的找是很愚蠢的。:-(
看起来比较聪明的办法是,首先预处理出,每个点向上跳2^j次,会跳在哪里。根据我们的生活常识,我们可以通过这些2^(i、j、k…)组合出任意一个正整数,也就是说我们可以通过每次跳2^j次跳出我们想要的。

    //首先dfs预处理出点的深度,以及向上跳2^0步所到达的点(就是这个点的父亲)。
    void dfs(int node)
    {
        for(int i=head[node];i;i=nxt[i])
        {
            if(depth[to[i]])continue;
            fat2[to[i]][0]=node;
            depth[to[i]]=depth[node]+1;
            dfs(to[i]);
        }
    }

然后我们还要求出跳2^j次所到达的点——

    //倍增求出跳2^j(j>=1 && ( 1 <<j) <=n )次所到达的点
    for(int j=1;(1<<j)<=n;j++)
        for(int i=1;i<=n;i++)
            if(fat[i][j-1]!=-1)
                fat[i][j]=fat[fat2[i][j-1]][j-1];

    //fat[i][j]表示从i结点开始,向上跳2^j步,所到达的结点编号

预处理就这样完了:-)

我是一棵树

再回到这张图。首先我们讨论的同一层的结点,比如说4和6,我们将他们一起向上跳2^20(这个数字可以按照题目数据规模更改)次,结果发现跳出去了…于是我们跳2^19次,2^18次……直到我们第一次跳到了这样的一层——对于所查询的x,y,在这一层的祖宗x1,y1不是同一个。
可以用脚趾头想到,x1与y1的最近公共祖先,就是x,y的最近公共祖先。然后我们就从x1,y1继续向上跳, 可以用脚趾头想到,由于我们是第一次跳到这样的一层,x与x1的深度差异,一定比x1与x1,y1的最近公共祖先的深度差异小。
假设我们是跳了2^j次跳到了x1,我们只要以x1为起点,从2^(j-1)次开始跳。可以用手趾头想到,这个时候我们跳到的两个点,xn,yn的父亲,就是x,y的最近公共祖先。

是的就这样。^_^

由此我们很容易的就解决了,当x,y深度一样时,查询x与y的最近公共祖先问题。但是更多的时候,x,y是不在同一层的。此时,我们可以运用上面的思想,将深度较大的点向上跳,使得两个点处于同一深度,转换成上面的问题。

查询代码如下:

    int find_lca(int x,int y)
    {
        if(depth[x]<depth[y])swap(x,y);
        for(int j=20;j>=0;j--)
            if(depth[x]>=depth[y]+(1<<j))
                    x=fat[x][j];
        if(x==y)return x;
        for(int j=20;j>=0;j--)
            if(fat[x][j]!=-1 && fat[x][j]!=fat[y][j])
            {
                x=fat[x][j];
                y=fat[y][j];
            }
        return fat[x][0];
    }

如果有错误,错误是我的:-(

RMQ

由于我不知道怎么长篇大论描述这个算法的神奇,所以我们从左至右来dfs这棵树吧。

我是一棵树

经过dfs,我们可以得到这样的一些数组:

first[i]表示第一次访问i结点的次序
ver[i]表示第i次访问了哪个结点
depth[i]表示ver[i]所表示点的深度

对于上面这棵树,这些数组是这样的(都忽略[0]):

first[]=1,2,14,3,5,15,6,10,7
ver[]=1,2,4,2,5,7,9,7,5,8,5,2,1,3,6,3,1
depth[]=1,2,3,2,3,4,5,4,3,4,3,2,1,2,3,2,1

对于任意两个点x,y中,假设first[x] < first[y],则必有first[x] <= ( ver[ ? ]= LCA(x,y) ) <= first[y]。为了更好地理解可以用纸自己画一画。
知道这一点后,求最近公共祖先的问题转化为,求?点满足ver[z]=? , first[x]<= ( ver[z] ) <=first[y],且depth[z]最小。
显然,我们可以用RMQ解决这个问题。:-)

代码如下:

    void dfs(int node,int dep)
    {
        ver[++tot]=node;
        first[node]=tot;
        depth[tot]=dep;  
        vis[node]=1;

        for(int i=head[node];i!=-1;i=nxt[i])  
        {  
            if(vis[to[i]])continue;
            dfs(to[i],dep+1);
            ver[++tot]=node;
            depth[tot]=dep;  
        }
    }


    void init(int nn)//nn为访问次数
    {
        for(int i=1;i<=nn;i++)dp[i][0]=i;
        memset(vis,0,sizeof(vis));
        dfs(1,1);

        for(int j=1;(1<<j)<=nn;j++)
            for(int i=1;i+(1<<j)-1<=nn;i++)
            {   
                int a = dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
                if(depth[a]<depth[b])dp[i][j]=a;
                else dp[i][j]=b;
            }
    }

事实上,在实际的操作中,我们并不需要ver数组,first数组就已经能够很好地满足我们的要求了。

查询代码如下:

    int askrmq(int left,int right)
    {
        int i=0;
        while((1<<(i+1))<=right-left+1)i++;
        int a=dp[left][i],b=dp[right-(1<<i)+1][i]; 
        if(depth[a]<depth[b])return a;
        return b;
    }

    int find_lca(int x,int y)
    {
        x=first[x];y=first[y];
        if(x>y)swap(x,y);
        return ver[askrmq(x,y)];
    }

如果有错误,请让我吃掉它:-(

最后附上几道题:

求树上两点之间距离 poj 1986 Distance Queries 点我点我:-)

求树上两点之间距离加强版 hdu 2874 Connections between cities 点我点我:-)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值