天天看點

倍增(線上)求LCA倍增求LCA代碼實作

這幾天,提高B組總是有求LCA的題。由于我是蒟蒻,是以老是做不出來,直接上暴力。現在才弄懂。

沒耐心看前面部分的大神門可以直接看後面。

ST(RMQ)算法(線上)求LCA

LCA是什麼?

在一棵樹上,兩個節點的最近公共祖先就是LCA。

求LCA有什麼用?

我見到最多的是,在一些題目中,我們需要找出樹上兩個點之間的路徑,其中就要借助LCA,作為一個中轉點。

舉個例子:

我們要找出兩個紅色的點之間的路徑。

倍增(線上)求LCA倍增求LCA代碼實作

黃色的這條路就是我們要求的。

倍增(線上)求LCA倍增求LCA代碼實作

怎麼找?

暴力方法1

BFS或DFS周遊一遍。時間複雜度顯然是O(N)的。

但我們要記住,這不是圖,而是一棵樹!

這是一棵樹,是以每兩個點之間一定有一個中轉點(可能是它們本身)!

這個中轉點就是它們的最近公共祖先。(圖中綠色的那個點)

倍增(線上)求LCA倍增求LCA代碼實作

兩個點之間的路徑顯然。

怎麼求LCA?

暴力方法2

先dfsO(N)記錄它的父親。

兩端同時暴力往上跳,每到一個點就打一個标記,跳到打過标記的點時退出,這個點就是LCA。

倍增(線上)求LCA倍增求LCA代碼實作

但速度較慢。設兩個點為x和y,深度為deep[x]和deep[y]。那麼将最多會有abs(deep[x]-deep[y])個沒有用的點被搜到(比如這個圖的第5步實際上是沒用的)。那麼,我們能不能不搜到這些沒用的點?

當然可以!

暴力方法3

首先用dfsO(N)預處理出每個點的深度(它們的父親也可以同時處理)。

先挑一個比較深的點,往上跳到與另一個點深度相同的位置。然後兩邊同時往上面暴力,相遇的點即答案。

倍增(線上)求LCA倍增求LCA代碼實作

然而還是過不了。看看例題(LCA模闆題)。這種方法隻有70分。因為每次都要搜一遍,很慢。

如果資料出了一條鍊來卡,就跑得超慢。

這也不行,那怎麼辦?(讀者:說了這麼久還是在将暴力,你幾個意思啊?)

倍增求LCA

求LCA有幾種方法,在網上我見到了tarjan(離線),RMQ轉LCA,還有樹鍊剖分。我介紹一個方法,叫倍增。

設f[i][j]表示點i往上的第2^j個祖先。

首先我們用dfsO(NlgN)求出f數組。式子:f[i][j]=f[f[i][j-1]][j-1]。不解釋。

然後我們就可以優美地倍增啦!首先,原來的套路,将兩個點跳到同一深度(跳到同一深度的過程也是幾個幾個跳)。然後将j從大到小枚舉,若f[x][j]!=f[y][j],則跳過去。否則就别跳,不然可能會跳過LCA。

最終的答案為f[x][0](f[y][0]一樣)。因為在這種限制下,不可能出現x==y的情況,除它們在同一條鍊上,如下圖

倍增(線上)求LCA倍增求LCA代碼實作

這種情況可以特判。因為你在統一它們的深度後,它們就已經重合了。

時間複雜度:O(NlgN+QlgN)

空間複雜度:O(NlgN)

NlgN為dfs預處理的時間,Q是詢問次數。

代碼實作

例題 P3379【模闆】最近公共祖先(LCA)

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
int n,m,s;
struct EDGE
{
    int x,y;
    EDGE* las;
} e[];//前向星存邊
int ne;
EDGE* last[];
int f[][];
int deep[];
void make_tree(int,int,int);
int main()
{
    scanf("%d%d%d",&n,&m,&s);
    int i,x,y;
    for (i=;i<n;++i)
    {
        scanf("%d%d",&x,&y);
        e[++ne]={x,y,last[x]};
        last[x]=e+ne;
        e[++ne]={y,x,last[y]};
        last[y]=e+ne;
    }
    make_tree(s,,);
    int j,k,tx,ty;
    for (i=;i<=m;++i)
    {
        scanf("%d%d",&x,&y);
        if (deep[x]<deep[y])
            swap(x,y);//確定x為深度較大的那個點
        k=deep[x]-deep[y];
        j=;
        while (k)
        {
            if ((k&))
                x=f[x][j];
            k>>=;
            ++j;
        }//這段代碼起了将兩點的深度統一的作用。不知道這樣打的原因的同學可以想想快速幂。當然也可以向下面那樣打for。兩種都可以。
        if (x==y)
        {
            printf("%d\n",x);
            continue;
        }
        for (j=int(log2(deep[x]));j>=;--j)//若這裡像上面那樣打while會錯。原因不解釋。
            if (f[x][j]!=f[y][j])
            {
                x=f[x][j];
                y=f[y][j];
            }
        printf("%d\n",f[x][]);
    }
}
void make_tree(int t,int fa,int de)
{
    f[t][]=fa;
    int i,j;
    for (i=,j=;j<=de;++i,j<<=)
        f[t][i]=f[f[t][i-]][i-];//處理處f數組
    deep[t]=de;
    EDGE* ei;
    for (ei=last[t];ei;ei=ei->las)
        if (ei->y!=fa)
            make_tree(ei->y,t,de+);
}
           
LCA