1. 程式人生 > >對LCA、樹上倍增、樹鏈剖分(重鏈剖分&長鏈剖分)和LCT(Link-Cut Tree)的學習

對LCA、樹上倍增、樹鏈剖分(重鏈剖分&長鏈剖分)和LCT(Link-Cut Tree)的學習

LCA

what is LCA & what can LCA do

,即最近公共祖先
在一棵樹上,兩個節點的深度最淺的公共祖先就是LCALCA (自己可以是自己的祖先)
很簡單,我們可以快速地進入正題了

下圖中的樹,4455LCALCA21133LCALCA1(1也算1的祖先)

除了是直接求兩點的LCALCA模板題,沒有題目會叫你打個模板

那麼LCA能幹嘛?

其實LCALCA用途很廣,感覺用LCALCA最多的像是這樣子的題

  • 給你一棵樹 (或者是一個圖求完MST,這很常見) 和樹上的兩點

  • 然後找出這兩點之間的路徑

    極值權值和或其他東東

這時我們可以藉助這兩點的LCA作為中轉點來輕鬆地解決這類題目

how to discover LCA

還是這棵樹

在這裡插入圖片描述

4433LCALCA1,這兩點間的路徑是下圖示紅的路徑

怎麼求LCA呢?

method one

暴力dfs一遍,時間複雜度O(n)O(n)
侮辱智商
(逃)

method two

模擬,從兩點開始一起向上跳一步,跳過的點標記一下,時間複雜度O(n)O(n)
侮辱智商*2
(逃)

method three

dfs記錄每個點的深度,求LCALCA時先調至統一深度,再一起向上跳
如果這棵樹是一個剛好分成兩叉

的樹,時間複雜度O(n)O(n)
侮辱智商*3
(逃)

上面的都是小學生都會的東西,不必講了
想真正用有意義LCALCA,接下來的內容才有點意思

LCALCA有一種非常容易理解的方法就是tarjan演算法預處理離線解決,時間複雜度O(n+q)O(n+q)飛速

仍感覺離線演算法不適用於所有題目
(比如某些良心出題人喪病地要你強制線上,GG

反正線上演算法可以解決離線能做出來所有的題

所以,我不講tarjantarjanLCALCA了,推薦一篇 寫的很棒的blog自行腦補一下
接下來的幾個線上演算法才是搞LCALCA

的門檻

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

這種方法的思想,就是將LCA問題轉化成RMQ問題

  • 如果不會RMQRMQ問題——STST演算法的就戳這裡

可以轉化成RMQ問題?

這裡有棵樹

如何預處理呢?

我們用dfs遍歷一次,得到一個dfsdfs序(兒子節點回到父親節點還要再算一遍
dfsdfs序就是這樣的1->2->4->7->4->8->4->2->5->2->6->9->6->10->6->2->1->3->1
(一開始在rootroot向兒子節點走,到了葉子節點就向另一個兒子走,最後回到rootroot

dfsdfs預處理的時間複雜度O(n)O(n)

dfs序妙啊

r[x]r[x]表示xxdfsdfs序當中第一次出現的位置,depth[x]depth[x]表示xx的深度
如果求xxyyLCALCAr[x]~r[y]這一段區間內一定有LCA(x,y)LCA(x,y),而且一定是區間中深度最小的那個點

(比如上面的dfs序中,第一個77和第一個55之間的序列裡的深度最小的點是22,而22正是7755LCALCA!)

  • 遍歷以LCA(x,y)LCA(x,y)為根的樹時,不遍歷完所有以LCA(x,y)LCA(x,y)為根的樹的節點是不會回到LCA(x,y)LCA(x,y)

  • 還有就是明顯地,想到達x再到y,必須上溯經過它們的LCA(兩個點之間有且只有一條路徑

  • 所以LCALCA的深度一定最小

直接用RMQ——ST表維護這個東西,求出來的最小值的點即為LCALCA

matters need attention

  • dfsdfs序的長度是2n12n-1,用dfsO(n)dfsO(n)處理出rrdepthdepthdfsdfs序之後直接套上裸的RMQRMQ
    (線段樹?差不多的……別在意)

  • f[i][j]f[i][j]表示dfs序中j~j+2^i-1的點當中,depth值最小的是哪個點 即可

  • 那麼單次詢問時間複雜度O(1)O(1)

  • 完美

code

宣告一下就是我也是看別人blog學的RMQ解決LCA,所以code我就直接copy過來了
code有問題,不能怪我
還有一點就是這人的code真提莫醜

#include <cstdio>
#include <cstring>
#include <cmath>

using namespace std;

int n,_n,m,s;//_n是用來放元素進dfs序裡,最終_n=2n-1

struct EDGE
{
    int to;
    EDGE* las;
}e[1000001];//前向星存邊

EDGE* last[500001];
int sx[1000001];//順序,為dfs序
int f[21][1000001];//用於ST演算法
int deep[500001];//深度
int r[500001];//第一次出現的位置

int min(int x,int y)
{
	return deep[x]<deep[y]?x:y;
}

void dfs(int t,int fa,int de)
{
    sx[++_n]=t;
    r[t]=_n;
    deep[t]=de;
    EDGE*ei;
    for (ei=last[t];ei;ei=ei->las)
        if (ei->to!=fa)
        {
            dfs(ei->to,t,de+1);
            sx[++_n]=t;
        }
}

int query(int l,int r)
{
    if (l>r)
    {
    	//交換 
        l^=r;
        r^=l;
        l^=r;
    }
    int k=int(log2(r-l+1));
    return min(f[k][l],f[k][r-(1<<k)+1]);
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    int j=0,x,y;
    for (int i=1;i<n;++i)
    {
        scanf("%d%d",&x,&y);
        e[++j]={y,last[x]};
        last[x]=e+j;
        e[++j]={x,last[y]};
        last[y]=e+j;
    }
    dfs(s,0,0);
    //以下是ST演算法
    for (int i=1;i<=_n;++i)f[0][i]=sx[i];
    int ni=int(log2(_n)),nj,tmp;
    for (int i=1;i<=ni;++i)
    {
        nj=_n+1-(1<<i);
        tmp=1<<i-1;
        for (j=1;j<=nj;++j)
            f[i][j]=min(f[i-1][j],f[i-1][j+tmp]);
    }
    //以下是詢問,對於每次詢問,可以O(1)回答
    while (m--)
    {
        scanf("%d%d",&x,&y);
        printf("%d\n",query(r[x],r[y]));
    }
}

小結

基礎的LCALCA知識你應該已經會了吧
LCALCA的運用挺廣吧,感覺和線段樹的考頻有的一比,不掌握是會吃虧的
上面的是比較普通的方法求LCALCA,其實樹上倍增也能求LCALCA
接下來到喪心病狂 (其實很簡單) 的樹上倍增了

樹上倍增

又是什麼東東?

倍增這個東東嚴格來說就是種思想,只可意會不可言傳
倍增,是根據已經得到了的資訊,將考慮的範圍擴大,從而加速操作的一種思想

使用了倍增思想的演算法有

  • 歸併排序
  • 快速冪
  • 基於ST表的RMQ演算法
  • 樹上倍增找LCA等
  • FFT、字尾陣列等高階演算法
  • ……FFTFFT有倍增的麼……我可能學了假的FFTFFT

some advantages

樹上倍增和RMQRMQ比較相似的,都採用了二進位制的思想,所以時空複雜度低
其實樹上倍增樹鏈剖分在樹題裡面都用的很多

  • 兩個都十分有趣,但倍增有著顯而易見的優點——

  • 比起樹鏈剖分,樹上倍增程式碼短,查錯方便,時空複雜度優(都是O(nlog2n)O(nlog_2n)

  • 只是功能欠缺了一些不要太在意

即使如此,樹上倍增也能夠解決大部分的樹型題目
反正兩個都要學

樹上倍增(線上)求LCA

preparation

怎麼樣預處理以達到線上單次詢問O(log2n)O(log_2n)的時間呢?

我們需要構造倍增陣列

  • anc[i][j]anc[i][j]表示i節點的第2^j個祖先

  • 注意ancanc陣列開到anc[n][log2n]anc[n][log_2n],因為樹上任意一點最多有2log2n2^{log_2n}個祖先

  • 明顯地發現這個東西完全可以代替不路徑壓縮的並查集anc[i][0]=father(i)∵anc[i][0]=father(i)
    (若anc[i][j]=0anc[i][j]=0則說明ii的第2j2^j祖先不存在)

然後,倍增的性質 DP方程 就清楚地出來了anc[i][j]=anc[anc[i][j1]][j1]anc[i][j]=anc[anc[i][j-1]][j-1]

  • 用文字來表達它就是這樣的

  • i的第2j2^j個父親是i的第2j12^{j-1}個父親的第2j12^{j-1}個父親

神奇不?

就是說暴力求ii的第kk個祖先的時間複雜度是O(k)O(k),現在變成了O(log2k)O(log_2k)
同時,用一個dfs處理出每個點的深度depth[x]depth[x]anc[x][0]anc[x][0],時間複雜度O(n)O(n)
這樣預處理的總的時間複雜度即為O(nlog2n)O(nlog_2n)

code

procedure dfs(x,y:longint);
var
        now:longint;
begin
        now:=last[x];
        while now<>0 do
        begin
                if b[now]<>y then
                begin
                        anc[b[now],0]:=x;
                        depth[b[now]]:=depth[x]+1;
                        dfs(b[now],x);
                end;
                now:=next[now];
        end;
end;
		for j:=1 to trunc(ln(n)/ln(2)) do
        for i:=1 to n do
                anc[i,j]:=anc[anc[i,j-1],j-1];

query

請務必認認真真地閱讀以下內容否則樹上倍增你就學不會了

樹上倍增的運用中,最簡單最實用的就是求LCALCA

  • 現在我們需要求出LCA(x,y)LCA(x,y),怎麼用樹上倍增做?

還記得前面三種 腦殘 O(n)O(n)LCALCA的第三個方法嗎?

用dfs記錄每個點的深度,求LCA時先調至統一深度,再一起向上跳

其實樹上倍增運用的就是這個思想!只不過時間複雜度降至了飛快的O(log2n)O(log_2n)