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

最近公共祖先(LCA)

\(最近公共祖先(LCA)\)

閒談

原因

這幾年\(NOIP\)考樹考的好多,打算寫幾篇部落格來增強記憶。\(NOIP rp++\)

背景

在樹上的問題中,對兩個點展開的有很多,\(LCA\)在很多時候會起到很大的作用。

演算法

概念

首先我們要談談什麼是最近公共祖先:對於有根樹\(T\)的兩個結點\(u、v\),最近公共祖先\(LCA(T,u,v)\)表示一個結點\(x\),滿足\(x\)\(u\)\(v\)的祖先且\(x\)的深度儘可能大。在這裡,一個節點也可以是它自己的祖先。也可以理解為距離兩個節點最近的公共祖先。所以它主要是用來處理當兩個點有唯一一條確定的最短路徑時的路徑。這裡我們就不介紹其他一些演算法了,只談倍增的方法。

倍增\(LCA\)

首先我們來理解一下什麼是倍增,倍增就是按照\(2\)的倍數來增長,\(1, 2, 4, 8, 16, 32……\),但我們在用倍增求解\(LCA\)的時候並不從小到大,而是從大到小來進行,\(……32, 16, 8, 4, 2, 1\),我們用這樣一個圖來更好的理解

(圖片來源於網路)
在這顆樹中,\(17\)\(18\)\(LCA\)就是\(3\)
暴力的演算法就是讓兩者分別向上一個一個竄,直到兩者相遇。但是這個時間複雜度太大了,我們就需要用到倍增的演算法了。
我們從大往小了跳,如果大了,那麼我們就調小, 這回它的時間複雜度為\(O(nlogn)\)足夠我們在比賽中使用了,那麼該如何實現呢。
首先我們需要記錄每個節點他們的深度\(depth[x]\)

和他們的\(2^i\)級祖先\(fa[i][j]\)(表示節點\(i\)\(2^j\)級祖先)。

void dfs(int now, int fath){    //now表示當前節點,fath表示它的父親節點
    fa[now][0] = fath;  //2的0次方為1,即它的父親節點
    depth[now] = depth[fath] + 1;   //now的深度為它父親的深度加1
    for(int i = 1; i <= lg[depth[now]]; i++)    //lg陣列的大小是指當前節點的深度的以2為底的對數,我們回溯只能回溯到根節點,所以需要加上這個判斷
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
        //這是整個演算法的核心,表示now的第2 ^ i個祖先為now的第2 ^ (i - 1)個祖先的2 ^ (i- 1)祖先
        //2 ^ i = 2 ^(i - 1) + 2 ^ (i - 1)
    for(int i = head[now]; i; i = e[i].next)    //我們用鏈式前向星來儲存整個圖
        if(e[i].to != fath) dfs(e[i].to, now);  //如果now當前節點指向的點不為它的父親,那麼則為它的兒子,這是我們now作為父節點再進行dfs,將它的子節點進行預處理
}

此時我們已經進行玩了預處理,這裡我們先將\(lg\)陣列求解一遍,進行常數優化。

for(int i = 1; i <= n; i++)
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);

在這之後就是倍增\(LCA\)了,我們先將兩個點提到同一高度,再統一的跳。注意這裡很重要,我第一遍學習的時候沒有理解透徹就是卡在了這裡。
但是我們不能直接跳到他們的\(LCA\),因為我們是從大到小進行跳的,可能最開始直接跳到了根節點,根節點肯定是兩個點的祖先,但是不是我們要求的最短公共祖先,所以我們要往下再跳一層,發現誒兩者不一樣了,這樣的話,這一層就是兩者的最短公共祖先。

int LCA(int x, int y){
    if(depth[x] < depth[y]) swap(x, y); //我們在這裡強制x的深度>=y的深度
    while(depth[x] > depth[y]) 
        x = fa[x][lg[depth[x] - depth[y]] - 1]; //首先將x和y跳到同一層
    if(x == y) return x;    //如果x是y的祖先,那麼他們的LCA就是x
    for(int i = lg[depth[x]] - 1; i >= 0; i--) //不斷的向上爬
        if(fa[x][i] != fa[y][i])      //我們要跳到的是它們的LCA的下一層,所以它們肯定不一樣,如果不相等就跳過去
            x = fa[x][i], y = fa[y][i];
    return fa[x][0];    //返回父節點
}

所以在這個圖中,我們按照倍增\(LCA\)的方法來求,路徑為:
\(17 -> 10 -> 7 -> 3\)
\(18 -> 16 -> 8 -> 5 -> 3\)
所以這就是整個過程了。

程式碼實現

#include<cstdio>
#include<iostream>
#define N 500010
using namespace std;
int n, m, s, cnt = 0;
int lg[N], head[N  << 1];
int depth[N], fa[N][22];
struct edge{
    int next, to;
}e[N << 1];

void add(int u, int v){
    e[++cnt].next = head[u];
    e[cnt].to = v;
    head[u] = cnt;
}

void dfs(int now, int fath){    //now表示當前節點,fath表示它的父親節點
    fa[now][0] = fath;  //2的0次方為1,即它的父親節點
    depth[now] = depth[fath] + 1;   //now的深度為它父親的深度加1
    for(int i = 1; i <= lg[depth[now]]; i++)    //lg陣列的大小是指當前節點的深度的以2為底的對數,我們回溯只能回溯到根節點,所以需要加上這個判斷
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
        //這是整個演算法的核心,表示now的第2 ^ i個祖先為now的第2 ^ (i - 1)個祖先的2 ^ (i- 1)祖先
        //2 ^ i = 2 ^(i - 1) + 2 ^ (i - 1)
    for(int i = head[now]; i; i = e[i].next)    //我們用鏈式前向星來儲存整個圖
        if(e[i].to != fath) dfs(e[i].to, now);  //如果now當前節點指向的點不為它的父親,那麼則為它的兒子,這是我們now作為父節點再進行dfs,將它的子節點進行預處理
}

int LCA(int x, int y){
    if(depth[x] < depth[y]) swap(x, y); //我們在這裡強制x的深度>=y的深度
    while(depth[x] > depth[y]) 
        x = fa[x][lg[depth[x] - depth[y]] - 1]; //首先將x和y跳到同一層
    if(x == y) return x;    //如果x是y的祖先,那麼他們的LCA就是x
    for(int i = lg[depth[x]] - 1; i >= 0; i--) //不斷的向上爬
        if(fa[x][i] != fa[y][i])      //我們要跳到的是它們的LCA的下一層,所以它們肯定不一樣,如果不相等就跳過去
            x = fa[x][i], y = fa[y][i];
    return fa[x][0];    //返回父節點
}

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

以上就是用倍增來實現\(LCA\)的方法了,只有勤加練習,才能夠對其有更深刻的理解和掌握。
完結撒花ヾ(✿゚▽゚)ノ