1. 程式人生 > 實用技巧 >LCA 最近公共祖先

LCA 最近公共祖先

LCA(不知道全稱叫什麼)

-倍增版-

作用:在一棵樹中求兩點公共祖先中深度最大的那個

  存在一顆 n 點的樹,詢問 m 次兩個點的最近公共祖先

複雜度:預處理O(nlogn),詢問O(logn)

主要思想:倍增

預處理:

  預處理出 f [ i ] [ j ] 陣列,表示 i 點向上 2j步的點,即自己往上數第 2j個祖先

  預處理出 dep [ i ] 陣列,表示以第 i 個點的深度

  

  這樣先給出一張以1為根的樣例樹,比較好解釋一點,這裡的值都是相對於根為1的情況,預處理的全部內容以該樹為參考

  那麼 f [ 12 ] [ 0 ] = 8 , f [ 12 ] [ 1 ] = 5 , f [ 12 ] [ 2 ] = 1

  那麼 dep [ 1 ] = 1 , dep [ 5 ] = 3 , dep[ 13 ] = 5

  i 點向上 2j相當於 i 點往上數的第 2j-1個祖先向上 2j-1

  預處理轉移方程:

for(int i=1;(1<<i)+1<=dep[u];++i)
        f[u][i]=f[f[u][i-1]][i-1];

  dep [ ] 陣列的處理用dfs完成,於是在處理dep [ ] 的同時,可以順便處理 f [ ] [ ]陣列

  那麼預處理只需要一個dfs即可完成。

  注意很多時候給邊的時候未給定邊的方向(比如只說u點到v點有一條邊,沒說明是u到v或v到u),建邊要雙向,由一個根開始的dfs來確定每條邊的方向。

void dfs(int u,int fa){
    v[u]=true;
    f[u][0]=fa;
    dep[u]=dep[fa]+1;
    for(int i=1;(1<<i)+1<=dep[u];++i)
        f[u][i]=f[f[u][i-1]][i-1];
    for(int i=head[u];i;i=edge[i].nxt)
        if(!v[edge[i].to])
            dfs(edge[i].to,u);
}

int main(){
    dfs(1,0);      
}

  注意dfs中 fa 是 u 的父親。

  為什麼dfs的開始可以 0 是 1 的父親開始呢?

  這樣預處理難道不會出現問題嗎?

  還真沒有,將 0 定為 1 的父親後,由於第一個for中(1<<i)+1<=dep[u] 所以除了 1 的點全部不會將 0 作為父親

  即使全把 0 做父親也沒有事情,因為最近公共祖先的定義保證了兩個點最大的公共最先頂多是根,不會到 0

查詢:

  在講查詢之前先來說說倍增的思想

  一個 x 個單位長的區間,可以分解為幾個不同的 2k的長度的小區間

  如14可以拆成8+4+2

  

  同樣的,我們想到從0到14可以拆解為如上三個步驟,先走 23步再走 22 步再走 21

  相關知識可以證明這個步數的組成是不同的且唯一的 (請查閱小學奧數)

  如此一來在一棵樹中,求一個點往上走 q 步到達的祖先只需要 logq 的複雜度

  我們考慮 x,y兩點處在共同的深度時

  他們有許多的公共祖先,若 f [ x ] [ k ]與f [ y ] [ k ] 相同,代表他們向上走 2k步的點是他們的公共祖先

  

  如圖a 是x、y的最近公共祖先,a的祖先都是x、y的祖先,p、q是 a 兒子且dep[p]=dep[q]=dep[a]+1

  LCA的大致思想是不斷更新x,y的位置,讓他們向上爬,p是我們希望x最終到達的位置,q是我們希望y最終到達的位置。

  我們每次確定一個值k,表示讓x,y都爬 2k步,之前說了x到p與y到q的路徑可以拆分成不同的2k,所以讓k改變,判斷這次的k是否需要爬即可

  可以發現如果走到a或a的祖先(x、y的公共祖先)時,x和y的位置變為相同,需要爬的部分在於a以下

int same_LCA(int x,int y){
    int k=log2(dep[x])+1;
    while(k>=0){
        if(f[x][k]!=f[y][k]) //本次爬完完在x~p和y~q的範圍內
            x=f[x][k],y=f[y][k];  //爬
        --k;
    }      
   //此時的x,y即為p,q的位置 }

  那麼此時的fa[x][0]= p往上爬一步= a號點 =最近公共祖先

  

  我們再考慮 x,y 不同深度

  那麼簡單,把x,y調成同深度即可,原理與上面相同

if(dep[x]<dep[y]) swap(x,y);
    while(dep[x]>dep[y]){
        int l=log2(dep[x]-dep[y]);
        x=f[x][l];
    }

  

然後再貼個程式碼,模板題是[洛谷 P3379 【模板】最近公共祖先(LCA)]

#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;

int n,m,s,edge_num;
int head[maxn],f[maxn][21],father[maxn],dep[maxn];
bool v[maxn];
struct {
    int nxt,to;    
}edge[maxn];

void add_edge(int from,int to){
    edge[++edge_num].nxt=head[from];
    edge[edge_num].to=to;
    head[from]=edge_num;
}

void dfs(int u,int fa){
    v[u]=true;
    f[u][0]=fa;
    dep[u]=dep[fa]+1;
    for(int i=1;(1<<i)+1<=dep[u];++i)
        f[u][i]=f[f[u][i-1]][i-1];
    for(int i=head[u];i;i=edge[i].nxt)
        if(!v[edge[i].to])
            dfs(edge[i].to,u);
}

int LCA(int x,int y){
    if(dep[x]<dep[y]) swap(x,y);
    while(dep[x]>dep[y]){
        int l=log2(dep[x]-dep[y]);
        x=f[x][l];
    }
    if(x==y) return x;
    int k=log2(dep[x])+1;
    while(k>=0){
        if(f[x][k]!=f[y][k]) 
            x=f[x][k],y=f[y][k];
        --k;
    }
    return f[x][0];
}

int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=1;i<=n-1;++i){
        int u,v;
        scanf("%d%d",&u,&v);
        add_edge(u,v);
        add_edge(v,u);
    }
    dfs(s,0);
    for(int i=1;i<=m;++i){
        int u,v;
        scanf("%d%d",&u,&v);
        printf("%d\n",LCA(u,v));
    }
}