LCA(lowest common ancestor)問題

轉自:鼻子很帥的豬

題描述

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


發佈了28 篇原創文章 · 獲贊 11 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章