1. 程式人生 > >LCT(Link Cut Tree)總結

LCT(Link Cut Tree)總結

概念、性質簡述

首先介紹一下鏈剖分的概念
鏈剖分,是指一類對樹的邊進行輕重劃分的操作,這樣做的目的是為了減少某些鏈上的修改、查詢等操作的複雜度。
目前總共有三類:重鏈剖分,實鏈剖分和並不常見的長鏈剖分。

重鏈剖分

實際上我們經常講的樹剖,就是重鏈剖分的常用稱呼。
對於每個點,選擇最大的子樹,將這條連邊劃分為重邊,而連向其他子樹的邊劃分為輕邊。
若干重邊連線在一起構成重鏈,用樹狀陣列或線段樹等靜態資料結構維護。
這裡就不贅述;

實鏈剖分

同樣將某一個兒子的連邊劃分為實邊,而連向其他子樹的邊劃分為虛邊。
區別在於虛實是可以動態變化的,因此要使用更高階、更靈活的Splay來維護每一條由若干實邊連線而成的實鏈。
基於性質更加優秀的實鏈剖分,LCT(Link-Cut Tree)應運而生。
LCT維護的物件其實是一個森林。
在實鏈剖分的基礎下,LCT資磁更多的操作

同樣將某一個兒子的連邊劃分為實邊,而連向其他子樹的邊劃分為虛邊。
區別在於虛實是可以動態變化的,因此要使用更高階、更靈活的Splay來維護每一條由若干實邊連線而成的實鏈。
基於性質更加優秀的實鏈剖分,LCT(Link-Cut Tree)應運而生。
LCT維護的物件其實是一個森林。
在實鏈剖分的基礎下,LCT資磁更多的操作

  • 查詢、修改鏈上的資訊(最值,總和等)
  • 隨意指定原樹的根(即換根)
  • 動態連邊、刪邊
  • 合併兩棵樹、分離一棵樹
  • 動態維護連通性
  • 更多意想不到的操作(可以往下滑一滑)

LCT的主要性質如下:

    1. 每一個Splay維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷Splay得到的每個點的深度序列嚴格遞增。
      比如有一棵樹,根節點為

1">1(深度1),有兩個兒子2,3(深度2),那麼Splay有3種構成方式:
{12},{3}
{13},{2}
{1},{2},{3}(每個集合表示一個Splay)
而不能把1,2,3同放在一個Splay中(存在深度相同的點)

  • 每個節點包含且僅包含於一個Splay中

  •                       邊分為實邊和虛邊,實邊包含在Splay中,而虛邊總是由一棵Splay指向另一個節點(指向該Splay中中序遍歷最靠前的點在原樹中的父親)。
                          因為性質2,當某點在原樹中有多個兒子時,只能向其中一個兒子拉一條實鏈(只認一個兒子),而其它兒子是不能在這個Splay中的。
                          那麼為了保持樹的形狀,我們要讓到其它兒子的邊變為虛邊,由對應兒子所屬的Splay的根節點的父親指向該點,而從該點並不能直接訪問該兒子(認父不認子)。

    各種操作

    access(x)

    LCT核心操作,也是最難理解的操作。其它所有的操作都是在此基礎上完成的。
    因為性質3,我們不能總是保證兩個點之間的路徑是直接連通的(在一個Splay上)。
    access即定義為打通根節點到指定節點的實鏈,使得一條中序遍歷以根開始、以指定點結束的Splay出現。
    所以還是來幾張圖吧。
    下面的圖片參考YangZhe的論文
    有一棵樹,假設一開始實邊和虛邊是這樣劃分的(虛線為虛邊)

    那麼所構成的LCT可能會長這樣(綠框中為一個Splay,可能不會長這樣,但只要滿足中序遍歷按深度遞增(性質1)就對結果無影響)

     

    現在我們要access(N),把AN的路徑拉起來變成一條Splay。
    因為性質2,該路徑上其它鏈都要給這條鏈讓路,也就是把每個點到該路徑以外的實邊變虛。
    所以我們希望虛實邊重新劃分成這樣。

    然後怎麼實現呢?
    我們要一步步往上拉。
    首先把splay(N),使之成為當前Splay中的根。
    為了滿足性質2,原來N−O的重邊要變輕。
    因為按深度ON的下面,在Splay中ON的右子樹中,所以直接單方面將N的右兒子置為0(認父不認子)
    然後就變成了這樣——

    我們接著把N所屬Splay的虛邊指向的I(在原樹上是L的父親)也轉到它所屬Splay的根,splay(I)。
    原來在II下方的重邊I−K要變輕(同樣是將右兒子去掉)。
    這時候I−L就可以變重了。因為L肯定是在I下方的(剛才L所屬Splay指向了I),所以I的右兒子置為N,滿足性質1。
    然後就變成了這樣——

    I指向H,接著splay(H),H的右兒子置為I。

     

    H指向A,接著splay(A)A的右兒子置為H。

    A−N的路徑已經在一個Splay中了,大功告成!
    程式碼其實很簡單。。。。。。迴圈處理,只有四步——

      1. 轉到根;
      2. 換兒子;
      3. 更新資訊;
      4. 當前操作點切換為輕邊所指的父親,轉1
    inline void access(int x){
        for(int y=0;x;y=x,x=f[x])
            splay(x),c[x][1]=y,pushup(x);//兒子變了,需要及時上傳資訊
    }
    

    makeroot(x)

    只是把根到某個節點的路徑拉起來並不能滿足我們的需要。更多時候,我們要獲取指定兩個節點之間的路徑資訊。
    然而一定會出現路徑不能滿足按深度嚴格遞增的要求的情況。根據性質1,這樣的路徑不能在一個Splay中。
    Then what can we do?
    makeroot定義為換根,讓指定點成為原樹的根。
    這時候就利用到access(x)和Splay的翻轉操作。
    access(x)後xx在Splay中一定是深度最大的點對吧。
    splay(x)後,x在Splay中將沒有右子樹(性質1)。於是翻轉整個Splay,使得所有點的深度都倒過來了,x沒了左子樹,反倒成了深度最小的點(根節點),達到了我們的目的。
    程式碼

    inline void pushr(int x){//Splay區間翻轉操作
        swap(c[x][0],c[x][1]);
        r[x]^=1;//r為區間翻轉懶標記陣列
    }
    inline void makeroot(int x){
        access(x);splay(x);
        pushr(x);
    }
    

    關於pushdown和makeroot的一個相關的小問題詳見下方update(關於pushdown的說明)

    findroot(x)

    x所在原樹的樹根,主要用來判斷兩點之間的連通性(findroot(x)==findroot(y)表明x,y在同一棵樹中)
    程式碼:

    inline int findroot(R x){
        access(x); splay(x);
        while(c[x][0])pushdown(x),x=c[x][0];
    //如要獲得正確的原樹樹根,一定pushdown!詳見下方update(關於findroot中pushdown的說明)
        splay(x);//此處的問題詳見下方update(關於findroot中splay(x)的說明)
        return x;
    }
    

     

    同樣利用性質1,不停找左兒子,因為其深度一定比當前點深度小。

    split(x,y)

    神奇的makeroot已經出現,我們終於可以訪問指定的一條在原樹中的鏈啦!
    split(x,y)定義為拉出x−y的路徑成為一個Splay(窩以y作為該Splay的根)
    程式碼

    inline void split(int x,int y){
        makeroot(x);
        access(y);splay(y);
    }
    

      

    x成為了根,那麼x到y的路徑就可以用access(y)直接拉出來了,將y轉到Splay根後,我們就可以直接通過訪問y來獲取該路徑的有關資訊

    link(x,y)

    連一條x−y的邊(窩使x的父親指向y,連一條輕邊)
    程式碼

    inline bool link(int x,int y){
        makeroot(x);
        if(findroot(y)==x)return 0;//兩點已經在同一子樹中,再連邊不合法
        f[x]=y;
        return 1;
    }
    

     如果題目保證連邊合法,程式碼就可以更簡單

    inline void link(int x,int y){
        makeroot(x);
        f[x]=y;
    }
    

      

    cut(x,y)

    x−y的邊斷開。
    如果題目保證斷邊合法,倒是很方便。
    使xx為根後,y的父親一定指向x,深度相差一定是1。當access(y),splay(y)以後,x一定是y的左兒子,直接雙向斷開連線

    inline void cut(int x,int y){
        split(x,y);
        f[x]=c[y][0]=0;
        pushup(y);//少了個兒子,也要上傳一下
    }
    

     那如果不一定存在該邊呢?
    充分利用好Splay和LCT的各種基本性質吧!
    正確姿勢——先判一下連通性,再看看x,yx,y是否有父子關係,還要看xx是否有右兒子。
    因為access(y)以後,假如y與x在同一Splay中而沒有直接連邊,那麼這條路徑上就一定會有其它點,在中序遍歷序列中的位置會介於x與y之間。
    那麼可能x的父親就不是y了。
    也可能x的父親還是y,那麼其它的點就在x的右子樹中,就像這樣

    只有三個條件都滿足,才可以斷掉。

    inline bool cut(int x,int y){
        makeroot(x);
        if(findroot(y)!=x||f[x]!=y||c[x][1])return 0;
        f[x]=c[y][0]=0;
        pushup(y);
        return 1;
    }
    

      如果維護了sizesize,還可以換一種判斷

    inline bool cut(int x,int y){
        makeroot(x);
        if(findroot(y)!=x||sz[y]>2)return 0;
        f[x]=c[y][0]=0;
        pushup(y);
    }
    

      解釋一下,如果他們有直接連邊的話,access(y)以後,為了滿足性質1,該Splay只會剩下x,y兩個點了。
    反過來說,如果有其它的點,size不就大於2了麼?


    其實,還有一些LCT中的Splay的操作,跟我們以往學習的純Splay的某些操作細節不甚相同。
    包括splay(x),rotate(x),nroot(x)(看到許多版本LCT寫的是isroot(x),但我覺得反過來會方便些)
    這些區別之處詳見下面的模板題註釋。

    update(關於findroot中pushdown的說明)

    找根的時候,當然不能保證Splay中到根的路徑上的翻轉標記全放掉。
    所以最好把pushdown寫上。
    Candy巨佬的總結對pushdown問題有詳細的分析

    makeroot(x);
    if(findroot(y)==x)//後續省略
    

      這樣好像沒出過問題,那應該可以證明是沒問題的(makeroot保證了x在LCT的頂端,access(y)+splay(y)以後,假如x,y在一個Splay裡,那x到y的路徑一定全部放完了標記)
    導致很久沒有發現錯誤。。。。。。
    另外提一下,假如LCT題目在維護連通性的情況中只可能出現合併而不會出現分離的話,其實可以用並查集哦!(實踐證明findroot很慢)
    這樣的例子有不少,比如下面“維護鏈上的邊權資訊”部分的兩道題都是的。
    甚至聽到Julao們說有少量題目還專門卡這個細節。。。。。。XZY巨佬的部落格就提到了

    update(關於pushdown的說明)

    pushdown和makeroot有時候會這樣寫,常數小一點

    void pushdown(int x){
        if(r[x]){
            r[x]=0;
            int t=c[x][0];
            r[c[x][0]=c[x][1]]^=1;
            r[c[x][1]=t]^=1;
        }
    }
    void makeroot(int x){
        access(x);splay(x);
        r[x]^=1;
    }
    

      

    這種寫法等於說當x有懶標記時,x的左右兒子還是反的

    再次update,發現這種問題還是可以避免的,若用這種pushdown,findroot可以寫,這樣寫就好啦

    inline int findroot(int x){
        access(x);splay(x);
        pushdown(x);
        while(lc)pushdown(x=lc);
        splay(x);
        return x;
    }
    

      所以此總結以及下面模板裡的pushdown,常數大了一點點,卻是更穩妥、嚴謹的寫法

    //pushr同上方makeroot部分
    void