LCA(lowest common ancestor)问题

from:http://blog.163.com/zhaohai_1988/blog/static/209510085201263195947966/

           http://www.2cto.com/kf/201402/278182.html

           http://dongxicheng.org/structure/lca-rmq/

问题描述

    LCA:Least Common Ancestors(最近公共祖先),对于一棵有根树T(不一定是二叉树哦)的任意两个节点u,v,求出LCA(T, u, v),即离根节点root最远的节点x,使得x同时是u和v的祖先。

对于该问题,最容易想到的算法是分别从节点u和v回溯到根节点,获取u和v到根节点的路径P1,P2,其中P1和P2可以看成两条单链表,这就转换成常见的一道面试题:【判断两个单链表是否相交,如果相交,给出相交的第一个点。】。该算法总的复杂度是O(n)(其中n是树节点个数)。

本文介绍了两种比较高效的算法解决这个问题。

在线算法用比较长的时间做预处理,但是等信息充足以后每次回答询问只需要用比较少的时间。

在线算法是DFS+ST。其中ST(sparse-table)算法在RMQ算法那篇文章中有详细解释。

    RMQ:给出一个数组A,回答询问RMQ(A, i, j),即A[i...j]之间的最值的下标。  

离线算法:先把所有的询问读入,然后一起把所有询问回答完成。 (Tarjan算法 )


问题的解决
在线算法DFS+ST (思想是:将树看成一个无向图,u和v的公共祖先一定在u与v之间的最短路径上):

(1)DFS:从树T的根开始,进行深度优先遍历(将树T看成一个无向图),并记录下每次到达的顶点。第一个的结点是root(T),每经过一条边都记录它的端点(欧拉环游)。由于每条边恰好经过2次,因此一共记录了2n+1个结点,用E[1, ... , 2n+1]来表示。比如



    我们可以建立三个数组:
E[1, 2*N-1] - 对T进行欧拉环游过程中所有访问到的结点;E[i]是在环游过程中第i个访问的结点
L[1, 2*N-1] -  欧拉环游中访问到的结点所处的层数;L[i]是E[i]节点所在的层数
R[1, N] ------ R[i] 是E中结点i第一次出现的下标(任何出现i的地方都行,当然选第一个不会错)

(2)计算R:用R[i]表示E数组中第一个值为i的元素下标,即如果R[u] < R[v]时,DFS访问的顺序是E[R[u], R[u]+1, …, R[v]]。虽然其中包含u的后代,但深度最小的还是u与v的公共祖先。

(3)RMQ:当R[u] ≥ R[v]时,LCA[T, u, v] = RMQ(L, R[v], R[u]);否则LCA[T, u, v] = RMQ(L, R[u], R[v]),计算RMQ。

由于RMQ中使用的ST算法是在线算法,所以这个算法也是在线算法。

(图中的H数组就是R数组)


再拿一个更小的例子讲解。
将有向树看成无向树,对于 u 和 v 的最近公共主祖先,则可以证明,最近公共祖先必定在 u 通往 v 的最短路径上,并且是最短路径上深度最小的结点。先对树进行 DFS, 保存其 DFS 序列, 再在序列找深度最小的结点。

例如:
对于树 <V,E>, V= { 1, 2, 3, 4, 5 }, E= { <1,2>, <1,3>, <3,4>, <3,5> }
对其进行 DFS 访问,记录的一种 DFS 访问路径为( 用E[]记录 ):
E[i]: 1 2 1 3 4 3 5 3 1       对应的结点在树中的深度为( 用L[]记录 ):
L[i]: 0 1 0 1 2 1 2 1 0
对于求 u 和 v 的最近公共祖先,先找到 u 和 v 在E[]中第一次出现的位置(其实可以用一个数组R[]记录下来,如前文介绍), 如 u= 2, v= 3 时, 2 在 E[] 中第一次出现的位置在 E[] 中下标为 1, 3第一次有 E[] 中出现的下标为 3, 考虑 E[] 中,下标从 1 到 3 这一段, { 2, 1, 3 } 对应深度为 { 1, 0, 1 } 深度最小为 0, 对应的结点为 1, 所以 u= 2, v= 3 的最近公共祖先为 1。
 
对一个数组中某一段求最值可以用 RMQ 来做(用sparse-table的算法),所以 LCA 问题就转化为了 RMQ 问题了。
代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;
#define N 902

int n, m;
vector<int> map[N];
int E[N<<1], L[N<<1], R[N], flag[N], Min[N<<1][12], cnt, rt;

void dfs( int x, int h ){ //参数h是用来表示节点所在的层数的,在dfs的时候直接就给L[]数组赋值,巧妙。
    E[++cnt]= x; L[cnt]= h;
    if( !flag[x] ){ R[x]= cnt; flag[x]= 1; }//在数组R[]中记录第一次出现的位置
    
    for( size_t i= 0; i< map[x].size(); ++i ){
        int v= map[x][i];    
        if( flag[v]== 0 ) dfs( v, h+ 1 );
        E[++cnt]= x; L[cnt]= h; //注意此处啊,不是一般的深度优先遍历,这可是欧拉环游路径。
    }
}

void init(){
    for( int i= 1; i<= cnt; ++i ) Min[i][0]= i;
    for( int i= 1; 1<<i< cnt; i++ )
    for( int j= 0, s= 1<< (i-1); j+ s< cnt; j++ ){
        if( L[ Min[j][i-1] ]< L[ Min[j+ s][i-1] ] )Min[j][i]= Min[j][i-1];
        else Min[j][i]= Min[j+s][i-1];
    }
}

int rmq( int x, int y ){
    if( x> y ) x^= y^= x^= y;
    int d= y- x+ 1, t= 0;
    while( 1<<t <= d ) t++; t--;
    if( L[ Min[x][t] ]< L[ Min[y-(1<<t)+1][t] ] ) return Min[x][t];
    else return Min[y-(1<<t)+1][t];
}

int main(){
    while( scanf("%d",&n)!= EOF ){
        for( int i= 0; i<= n; ++i ) flag[i]= 0;
        cnt= 0;
        
        for( int i= 1; i<= n; ++i ){
            int u, num, v;
            scanf("%d",&u);
            while( getchar()!= '(');
            scanf("%d",&num);
            while( getchar()!= ')');
            
            while( num-- ){
                scanf("%d",&v );
                map[u].push_back(v); flag[v]++;
            }
        }
        
        for( int i= 1; i<= n; ++i )
        if( flag[i]== 0 ){ rt= i; break; }
        
        for( int i= 0; i<= n; ++i ) flag[i]= 0;
        dfs( rt, 0 );
        init();
        
        for( int i= 0; i<= n; ++i ) flag[i]= 0;
        scanf("%d",&m );
        for( int i= 1; i<= m; ++i ){
            while( getchar()!= '(' );
            int u, v;
            scanf("%d%d", &u, &v );
            
            int pos= rmq( R[u], R[v] );
            flag[ E[pos] ]++;
            while( getchar()!= ')' );
        }
        
        for( int i= 1; i<= n; ++i )
        if( flag[i] ) printf("%d:%d\n", i, flag[i] );
        
        for( int i= 0; i<= n; ++i ) map[i].clear();
    }
    return 0;
}

二、 离线算法Tarjan算法
  所谓离线算法,是指首先读入所有的询问(求一次LCA叫做一次询问),然后重新组织查询处理顺序以便得到更高效的处理方法。Tarjan算法是一个常见的用于解决LCA问题的离线算法,它结合了深度优先遍历和并查集,整个算法为线性处理时间。
同上一个算法一样,Tarjan算法也要用到深度优先搜索,算法大体流程如下:对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
   算法从根开始,对每一棵子树进行深度优先搜索,访问根时,将创建由根结点构建的集合,然后对以他的孩子结点为根的子树进行搜索,使对于 u, v 属于其某一棵子树的 LCA 询问完成。这时将其所有子树结点与根结点合并为一个集合。 对于属于这个集合的结点 u, v 其 LCA 必定是根结点。
  算法伪代码:
LCA(u){
    MAKE_SET(u)
    ancestor[FIND(u)]= u
    for( each child v of u ){
        LCA(v)
        UNION(u,v)
        ancestor[FIND(v)]=u
    }
    flag[u]= 1;
    for( each node v such that [u,v] in P )
    if( flag[v] ) 
    print "The least common ancestor of 'u' and 'v' is " ancestor[ FIND(v) ]
}
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
#define N 10010

vector<int> map[N];
int n, uset[N], ancestor[N], u, v, flag[N], deg[N], root;

//并查集的操作。
int find( int x ){

 if(uset[x]==x)
  return x;
 else 
  return uset[x]=Find(uset[x]);
}

void Union( int x, int y ){
    int a= find(x), b= find(y);
    uset[a]= b; }
    
void LCA( int x ){
    uset[x]= x; ancestor[x]= x;
    for( size_t i= 0; i< map[x].size(); ++i ){
        int y= map[x][i];
        LCA( y );
        
        Union( x, y );
        ancestor[ find(y) ]= x;
    }
    flag[x]= 1;
    if( x== u && flag[v] ){
        printf("%d\n", ancestor[ find(v) ] );
        return; }
    else if( x== v && flag[u] ){
        printf("%d\n", ancestor[ find(u) ] );
        return; }
}

int main(){
    int test;
    scanf("%d",&test );
    while( test-- ){
        scanf("%d",&n);
        
        for( int i= 0; i<= n; ++i ) { ancestor[i]= 0; flag[i]= 0; deg[i]= 0; }
        for( int i= 1; i< n; ++i ){
            int x, y;
            scanf("%d%d", &x, &y);
            map[x].push_back(y);
            deg[y]++;
        }
        scanf("%d%d",&u,&v);
        for( int i= 1; i<= n; ++i )
        if( deg[i]== 0 ) root= i;
    
        LCA( root );
        for( int i= 0; i<= n; ++i ) map[i].clear();
    }
    
    return 0;
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值