1. 程式人生 > >[學習筆記]動態樹Link-Cut-Tree

[學習筆記]動態樹Link-Cut-Tree

str 直接 不同 html 思想 .cn 學習筆記 普通 平衡樹

參考&&推薦:

[Link-Cut-Tree]【學習筆記】

一、概括

一種支持維護樹的森林的算法。

采用實鏈剖分,多棵splay維護每個實鏈,鍵值就是節點的深度。

即,中序遍歷就是這個鏈從上到下的節點組成。

同一個原樹上的splay之間也連接,但是不同的實鏈的splay之間的父子邊單向,由兒子指向父親。

即,認父不認子。

也就是說,一個節點可以成為多個節點的父親,但是最多只認一個左兒子,一個右兒子。

可以支持,

1.加入一條邊(連接兩棵樹),

2.刪除一條邊

3.處理一條路徑上的詢問。

二、核心思想

1.實鏈剖分

區別於重鏈剖分和長鏈剖分,實鏈剖分的實鏈是動態的。

即,輕兒子重兒子隨便換。根據需求會換不同的兒子。

2.原樹和輔助樹分開處理。

輔助樹即splay(森林)

一棵原樹可以認為是一棵部分聯通的splay,也可以看做是多個splay,之間認父不認子

(以下稱維護一條鏈的splay叫小splay,整個原樹的splay叫大splay)

原樹和輔助樹沒有區分開是初學者懵逼的一大原因。

splay的中序遍歷就是這個鏈從上到下的節點組成。

可以參考上面第二篇博客。

3.每次提取一條完整的鏈進行處理。兩端就是路徑的起始終止節點。

“兩端就是路徑的起始終止節點。”這句話尤為關鍵。

makert之後access之後splay就可以把一個鏈放進一個子樹裏,輕松查找。(類似於普通splay區間查詢)

還有一些性質,可以參考第一篇博客。

1.深度為鍵值,嚴格遞增(顯然,splay不可能存在兩個點鍵值相同)

2.每個點屬於一個小splay

3.認父不認子。最多一個重兒子

三、函數

1.access(靈魂函數)

access意為進入,接近。

就是打通一條root到x的路徑。把root到x的路徑變成實鏈。

無關的邊都變成輕鏈。

摘自:LCT總結——概念篇+洛谷P3690[模板]Link Cut Tree(動態樹)(LCT,Splay)

A是樹根,access(N)

技術分享圖片

技術分享圖片

這裏我們直接把輕邊變成重邊。

void access(int x){
    for(int y=0;x;y=x,x=t[x].fa){
        splay(x);t[x].ch[
1]=y;pushup(x); } }

開始扔掉右兒子,就是扔掉N-O這塊子樹。

發現一個關鍵的性質,

這樣,N和A在同一個小splay裏面,N和A的路徑上的點就是這個splay中的所有點。

2.splay(靈魂函數)&&rotate&&nrt

註意:這裏splay僅在小splay中進行。都是把x轉到這個小splay的根。

與一般splay不同的是,

LCT中splay的節點編號就是原樹節點編號。可以隨機訪問。沒有kth這一步

也就意味著,pushdown必須跟上。

用一個棧記錄父親,然後依次pushdown

之後像原來一樣splay即可。

void splay(int x){
    int y=x,z=0;
    sta[++z]=y;
    while(nrt(y)) y=t[y].fa,sta[++z]=y;
    while(z) pushdown(sta[z--]);
    
    while(nrt(x)){
        y=t[x].fa,z=t[y].fa;
        if(nrt(y)){
            rotate((t[y].ch[0]==x)==(t[z].ch[0]==y)?y:x);
        }
        rotate(x);
    }
    pushup(x);
}

值得註意的是,由於不能轉出去,所以必須有一個nrt(not root)判斷是否x是當前小splay的根。

通過father認不認這個x兒子判斷是否為根。

否,返回1,是,返回0

bool nrt(int x){
    return (t[t[x].fa].ch[0]==x)||(t[t[x].fa].ch[1]==x);
}

由於認父不認子,所以小splay的根的father不能設置兒子關系。

void rotate(int x){
    int y=t[x].fa,d=t[y].ch[1]==x;
    t[t[y].ch[d]=t[x].ch[!d]].fa=y;
    if(nrt(y)) t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x;//nrt註意 
    else t[x].fa=t[y].fa;//無論如何要設置x的fa 
    t[t[x].ch[!d]=y].fa=x;
    pushup(y);
}

3.makert&&reverse

由於深度單調遞增,所以實鏈一定是豎直往下的。

對於x到y的路徑,x,y可能不在一個實鏈上。

原樹是一棵無根樹,所以可以隨時換根。

引入makert函數,把x欽定成為原樹的根。

只要調整中序遍歷即可。

access(x),splay(x),然後reverse(x),那麽,就相當於直接翻轉。

那麽,本來x是和root相連的實鏈的底端,這樣reverse一下,直接變成了根!!

神奇操作。

void rev(int x){
    ls^=rs^=ls^=rs;
    t[x].r^=1;
}
void makert(int x){
    access(x);
    splay(x);
    rev(x);
}

4.findrt

查找一個點所在的原樹的根。

找中序遍歷第一個即可。

int findrt(int x){
    access(x);splay(x);
    while(t[x].ch[0]) x=t[x].ch[0];
    return x;
}

值得註意的是,原樹的根現在不是所屬小splay的根了,根變成了x

5.split

提出x到y的路徑。

根據access的得出的性質可以發現,makert(x),再access(y),就把x到y的路徑打通了。

然後splay(y),那麽這個路徑就是y和y的左子樹。

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

之後可以直接查詢y的信息。

6.link

連接原樹中x到y

判斷是否在同一個原樹裏。然後把x連向y一條認父親邊。

y先不認x這個兒子,必要的時候會access

void link(int x,int y){
    makert(x);
    if(findrt(y)==x) return;
    t[x].fa=y;
    pushup(y);
}

7.cut

判斷是否相連 。

makert再findrt後,如果相連,y一定是x的father,x一定是y的左兒子。

如果x的father不是y,或者x有右兒子。則沒有這條邊。

否則斷開。

void cut(int x,int y){
    makert(x);
    if(findrt(y)!=x||t[x].fa!=y||t[x].ch[1]) return;
    t[x].fa=t[y].ch[0]=0;
    pushup(y);
}

8.pushup

維護權值

void pushup(int x){if(x)t[x].s=t[rs].s^t[ls].s^t[x].v;}

9.pushdown

主要是下放reverse標記。

void pushdown(int x){
    if(t[x].r){
        t[x].r=0;
        rev(ls);rev(rs);
    }
}

四、比較區別&&優勢所在

1.與普通平衡樹

見splay函數區別。

2.樹鏈剖分、並查集比較

支持動態加邊,刪邊,還可以支持維護鏈的信息。

五、應用

支持的操作就是基本應用。

留坑。

[學習筆記]動態樹Link-Cut-Tree