$LCT$初步

Flower&)發表於2019-01-27

\(\rm{0x01}\) 閒話 · \(LCT\)的用途以及具體思路

LCT是啥?百度一下的話……貌似是一種檢查婦科病的東西?Oier的口味可是真不一般啊

咳,其實在我最近只是淺淺地學了一部分的基礎上,窩覺得\(LCT\)其實就是一個用來維護森林連通性的。

嗯……因為其獨特的性質所以也可以順便維護好多東西,什麼鏈上的最大值啊,鏈上的權值和啊……都可以維護——或者說,LCT是更加全能的樹剖。

但其實吧……\(LCT\)打板子是很簡單的,但是真正理解卻一點兒也不簡單。因為本身\(splay\)就很麻煩了,況且\(splay\)之前一直用於維護數列。要知道,此處的\(splay\)可是作為輔助樹,維護一整個森林,並且可以支援序列中幾乎全部操作——這就大大增高了理解難度。舉個例子,你曾經認為已經難以理解、或者說不可以做的、比較複雜的區間翻轉\(Luogu3391\),在\(LCT\)裡面有十分全面的涉及,但是被精簡到了只有兩行是用來描述這個內容的。顯而易見的是,\(LCT\)雖然常數十分的大,但程式碼十分的短,比一棵完整的平衡樹短了不少(實測50+行),與\(FFT\)一樣具有著華麗的可觀賞性,但是隱藏在之後的思維難度同樣不可小覷。

也就是說我們是不是學的太草率、太浮躁了呢?快餐式地學完\(LCT\),網上的每一篇部落格都包教包會。但是我今天要整理的,是對於\(LCT\)真正的理解。希望各位看到這篇拙作的人可以獲得一些什麼。

\(\rm{0x02}\) 閒話 · 關於\(\rm{splay}\)

道理我都懂,要想動態拆刪一些東西,輔助樹的形態可以改變是先決條件。看上去平衡樹好像是個不錯的選擇,但是,選哪個作為輔助樹呢?後宮佳麗三千我該翻誰的牌子呢

歷史的重任最後落到了\(\rm{splay}​\)的身上。然後\(\rm{splay}​\)他居然:

$LCT$初步

他甚至還:

$LCT$初步

……

好吧,由於某些rqy也不知道的原因,如果不用\(\rm{splay}\)的話,複雜度是均攤\(\Theta(\rm{nlog^2n})\), 而用\(\rm{splay}\)就可以做到均攤\(\Theta(\rm{nlogn})\) ……但事實上,splay確實有他獨特的性質,比如旋轉到根啊之類的,比起其他種類的平衡樹而言,更加適合\(LCT\)

\(\rm{0x03}\) \(LCT\)的思路和基礎操作

一 主要思路

主要思路嘛……大概是基於實鏈剖分的操作。

樸素的樹剖是重鏈剖分,大體上就是將整棵樹的鏈劃分為輕邊和重鏈,運用巧妙的性質做到\(log\)級別。而遺憾的是\(LCT\)維護的是森林的連通性,所以只能採用實鏈剖分。

而實鏈剖分大體上就是把邊分為虛邊實邊。其中實邊串聯起一個聯通塊,同一組實邊存在、且僅存在於一棵\(\rm{splay}\)中。\(\rm{splay}\)\(\rm{splay}\)之間由虛邊相連。

實鏈剖分的好處呢?在於實鏈剖分是一種動態剖分,他可以隨意改變邊的虛實屬性。而顯然,重鏈剖分由於有著足夠的理論依據和邏輯推演,所以輕重鏈是難以更改,或者說,不可更改的。So,實鏈剖分為動態樹的動態打下了基礎。

那麼接下來我們來看一個\(LCT​\)是如何定義的:

  • 首先,一棵\(LCT​\)管控的是一對分散的點,點以幾棵分散的\(splay​\)的形式聚集。起初整棵\(LCT​\)是沒有任何聯絡的,各自為戰,各自為根。我們接下來會看到的\(access​\)\(makeroot​\)等操作,都是在自己的聯通塊兒內部進行的操作。換句話講,\(LCT​\)維護的是有根森林,即組成森林的每個聯通塊都有其唯一的根。
  • 實邊串聯起一個聯通塊,同一組實邊存在、且僅存在於一棵\(\rm{splay}\)中。\(\rm{splay}\)\(\rm{splay}\)之間由虛邊相連。只有實邊是有效的,虛邊可以被認為不存在。但是兩種邊都沒有用到顯式儲存,都是通過splay中的\(Son\)陣列和\(Fa\)陣列訪問的。但虛邊和實邊的儲存有區別:
    • 虛邊是認父不認子,即如果\(Fa[x]==y\),那麼\(y\)不存\(x\)這個兒子,但是\(x\)\(y\)這個父親。這樣做是為了可以\(Access\)——因為其實在\(Access\)的子函式\(splay\)裡,發揮作用的實際上是\(Fa\)指標。
    • 實邊是完整的雙向儲存。
  • \(\rm{splay}\)中維護的是一條從儲存上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷\(\rm{splay}\)得到的每個點的深度序列嚴格遞增。換句話講,一個\(\rm{splay}\)裡面不會出現在原聯通塊兒(樹)中深度相同的兩個點。在一棵\(\rm{splay}\)中,鍵值就是原樹中的深度。
  • 如果\(x\)是它所在\(splay\)的最左邊的點,那麼它在原森林裡的父親是\(x\)所在\(splay\)的根的\(fa\), 否則就是\(x\)\(splay\)上的前驅.

二 基礎操作

\(emm\)所謂基礎操作大概就是每個用到\(LCT\)的題幾乎都要用到的操作,我們這個地方先把點\(n\)所在聯通塊兒內的樹的根記作$root(n) \(,把與\)n$以實邊相連的兒子記作實兒子。

  • \(\rm{1}\) \(Access\)

這個操作有著很迷的性質,其時間複雜度是均攤\(\log n\)的。而這個操作的目的是\(Access(n)\)表示從\(root(n)\)\(n\)打通一條實鏈,並以\(n\)點為最深度最大的點、\(root(n)\)為深度最小的點形成一棵\(\rm{splay}\)

不難看出,這個操作其實跟是一種邏輯層面的自我調控,沒有改變原樹的結構。

我們思考,如果此時我們\(Access​\)完點\(n​\)之後,理論上來講,\(n​\)點應該不再有實兒子了——顯然,如果有實兒子的話,\(splay​\)中是應該包含這個實兒子的——而這就不符合\(n​\)\(\rm{splay}​\)中深度最大的點的性質了。而因為在splay中,點是以深度為鍵值的,所以我們要每次砍掉\(\rm{splay}​\)中的右兒子——即砍掉原來的實兒子,並把剛剛誕生的\(\rm{splay}​\)連上。

inline void Access(int x) { 
    for (int qwq = 0 ; x ; x = T[qwq = x].F) 
        splay(x), rc = qwq, update(x) ; 
}

然後這就是\(Access​\)了。

  • \(2 ~~Make~ Root~\)

\(make\_root​\)先從原來的根向\(n​\)打通一條路徑,然後\(splay​\)上去,最後\(reverse​\)一下。此處由於一開始\(n​\)的深度最大,\(splay​\)之後深度依舊最大,但此時\(n​\)\(splay​\)的根,所以\(reverse(n)​\)就相當於翻轉了整條樹上的鏈,那麼翻轉之後,\(n​\)的深度就變成了最小,於是就是這個聯通塊兒的根節點了。

#define lc T[x].Son[0]
#define rc T[x].Son[1]
struct LCT{
    int F, Son[2], R, S ;
}T[MAXN] ; 
inline void splay(int x) ;
inline void reverse(int x) { lc ^= rc ^= lc ^= rc, T[x].R ^= 1 ;}
inline void push_down(int x) { if (!T[x].R) return ; T[x].R = 0 ; if (lc) reverse(lc) ; if (rc) reverse(rc) ; }

inline void Rooten(int x) { Access(x), splay(x), reverse(x) ; }

inline void splay(int x){
    int qwq = x ; stk.push(qwq) ;
    while(check(qwq)) qwq = T[qwq].F, stk.push(qwq) ;
    while(!stk.empty()) push_down(stk.top()), stk.pop() ;
    while(check(x)){
        int fa = T[x].F, g_fa = T[fa].F ;
        if (check(fa)) rotate((T[g_fa].Son[1] == fa) == (T[fa].Son[1] == x) ? fa : x) ; rotate(x) ;
    }
}

此處\(splay\)中由於要下放標記,保證樹的形態是正確的,所以我們用一個\(stack\)存一下,順序下放標記。

  • \(3~~Merge~~\)

此處的\(Merge(x, y)\)的意義是,拉起\(x,y\)中間的鏈,形成一個\(splay\)。這裡就直接\(Mkroot\)一遍,然後\(Access\)即可。讓哪個點當根應該都可以,只不過多\(splay\)幾次可以保證優(毒)秀(瘤)的復(大)雜(常)度(數)。

inline void Merge(int x, int y) { Rooten(x), Access(y), splay(y) ; }
  • \(4~~Link~\&~Cut\)

如果保證\(Link\)\(Cut\)都是合法的操作的話,\(Link\)直接連,\(Cut\)直接刪即可。

inline void Link(int x, int y){ Rooten(x) ;  T[x].F = y ;}
inline void Cut(int x, int y){ Merge(x, y) ; T[x].F = T[y].Son[0] = 0 ;}

此處\(Link\)必須先\(Mkroot\)一下,否則樹鏈就斷了。連的是虛邊(因為連實邊就會改變原來\(splay\)的割據);\(Cut\)必須先\(split\)一下,保證兩個點之間在同一棵\(splay\)中,加之我們的\(Merge\)操作中,一開始把\(x\)\(mkroot\)了,再把\(y\)\(splay\)上去,直接導致了現在\(x\)應該是\(y\)的孩子——於是就很開心的,可以直接\(cut\)了。

但事實上,天不遂人意……有時候這條邊並不存在,盲目刪除的話會導致\(GG\),盲目連邊的話也會導致樹的形態爆炸,所以我們要進行一波操作……

  • \(New-Link\)
inline void Link(int x, int y){ Rooten(x) ; if(Find(y) != x) T[x].F = y ;}
inline int Find(int x){ Access(x), splay(x) ; while(lc) push_down(x), x = lc ; splay(x) ; return x ;}

此處的意義在於,如果我們發現兩個點在一個子樹裡面,連線它們就破壞了樹的性質。\(Find\)就是無比普通的\(Find\)。。。。233

但要注意啊,\(Find\)找的是原樹中的根,不是\(splay\)。由於原樹中根的深度一定最小,所以應該是\(splay\)中最靠左的點……所以不斷找左兒子。

\(BB\)一句,這個地方一定注意啊!\(Find\)只改變了\(splay\)的形態,\(mkroot\)改變的是原樹中的根

  • \(New-Cut\)
inline void Cut(int x, int y){ 
    Rooten(x) ; 
    if (Find(y) != x || T[y].Son[0] || T[y].F != x) return ; 
    T[y].F = T[x].Son[1] = 0, update(x) ; 
}

此處首先我們要判一下這兩個點是不是直接相連。是否直接相連……在一棵\(splay\)中的體現,要克服兩個問題,第一是要判斷是否連通,還是\(Find\)操作。

之後我們需要判斷是否滿足恰好相隔一條邊——注意,首先因為程式碼中的\(x\)\(y\)在原樹位置靠上(\(Rooten\)\(x\)),在\(splay\)中靠左,那麼如果\(y\)有左兒子的話,說明一定有\(Depth(x) < Depth(y\text{的左兒子們}) < Depth(y)\),其中\(Depth\)表示原樹深度。那麼此時原樹中\(x\)\(y\)之間,一定隔著一些節點。考慮樹的性質,兩點之間有且僅有一條簡單路徑——所以當\(T[y].Son[0]\)不指向\(Null\)時,\(x\)\(y\)之間沒有一條邊,不能直接\(Cut\)

剩下的就很簡單了,\(T[y].F\)應該是\(x\),否則也不是直接相連。

  • 5 \(~Rotate\)中的坑點

呃……其實就一處而已。就是:

inline bool check(int x){ return T[T[x].F].Son[0] == x || T[T[x].F].Son[1] == x ;  }
inline void rotate(int x) {
    int fa = T[x].F, g_fa = T[fa].F, W = x == T[fa].Son[1] ;
    if (check(fa)) T[g_fa].Son[T[g_fa].Son[1] == fa] = x ; T[x].F = g_fa ;
    T[fa].Son[W] = T[x].Son[W ^ 1], T[T[x].Son[W ^ 1]].F = fa, T[fa].F = x, T[x].Son[W ^ 1] = fa, update(fa), update(x) ;
}

這個地方\(splay\)雙旋判斷祖父的時候,不再用\(\rm{if(g\_fa)}\),而是用\(\rm{if(check(fa))}\)。原因很簡單,我們的虛邊也是有指向父親的指標的,但是連線兩個不同的\(splay\)

剩下的……大概就沒了吧……

於是——

\(\color{red}{C}\color{cyan}{o}\color{gold}{d}\color{green}{e}\)

#include <stack>
#include <cstdio>
#include <iostream>

#define MAXN 300233
#define lc T[x].Son[0]
#define rc T[x].Son[1]
#define rep(a, b, c) for(a = b ; a <= c ; ++ a)

using namespace std ;
struct LCT{
    int F, Son[2], R, S ;
}T[MAXN] ; stack <int> stk ;
int base[MAXN], N, M, A, B, C, i ;

inline int Find(int x) ;
inline void splay(int x) ;
inline void push_down(int x) ;
inline void update(int x) { T[x].S = T[lc].S ^ T[rc].S ^ base[x] ;}
inline void reverse(int x) { lc ^= rc ^= lc ^= rc, T[x].R ^= 1 ;}
inline bool check(int x){ return T[T[x].F].Son[0] == x || T[T[x].F].Son[1] == x ;  }
inline void Access(int x) { for (int qwq = 0 ; x ; x = T[qwq = x].F) splay(x), rc = qwq, update(x) ; }
inline void rotate(int x) {
    int fa = T[x].F, g_fa = T[fa].F, W = x == T[fa].Son[1] ;
    if (check(fa)) T[g_fa].Son[T[g_fa].Son[1] == fa] = x ; T[x].F = g_fa ;
    T[fa].Son[W] = T[x].Son[W ^ 1], T[T[x].Son[W ^ 1]].F = fa, T[fa].F = x, T[x].Son[W ^ 1] = fa, update(fa), update(x) ;
}
inline void splay(int x){
    int qwq = x ; stk.push(qwq) ;
    while(check(qwq)) qwq = T[qwq].F, stk.push(qwq) ;
    while(!stk.empty()) push_down(stk.top()), stk.pop() ;
    while(check(x)){
        int fa = T[x].F, g_fa = T[fa].F ;
        if (check(fa)) rotate((T[g_fa].Son[1] == fa) == (T[fa].Son[1] == x) ? fa : x) ; rotate(x) ;
    }
}
inline void Rooten(int x) { Access(x), splay(x), reverse(x) ; }
inline void split(int x, int y) { Rooten(x), Access(y), splay(y) ; }
inline void Link(int x, int y){ Rooten(x) ; if(Find(y) != x) T[x].F = y ;}
inline int Find(int x){ Access(x), splay(x) ; while(lc) push_down(x), x = lc ; splay(x) ; return x ;}
inline void push_down(int x) { if (!T[x].R) return ; T[x].R = 0 ; if (lc) reverse(lc) ; if (rc) reverse(rc) ; }
inline void Cut(int x, int y){ Rooten(x) ; if (Find(y) != x || T[y].Son[0] || T[y].F != x) return ; T[y].F = T[x].Son[1] = 0, update(x) ; }
int main(){
    cin >> N >> M ;
    rep(i, 1, N) scanf("%lld", &base[i]) ;
    rep(i, 1, M){
        scanf("%d%d%d", &A, &B, &C) ;
        if (A == 0) split(B, C), printf("%d\n", T[C].S) ;
        else if (A == 1) Link(B, C) ; else if (A == 2) Cut(B, C) ; else splay(B), base[B] = C ;
    }
    return 0 ;
}

\(\rm{0x00}\) 後記和參考

可寫完了……嗝……打個肥宅嗝犒勞犒勞自己

怎麼說呢,自從我開始學\(LCT\)到我寫完這篇\(blog\)為止,是我十分難熬的時間,總時長接近一週。一開始看別人寫的\(LCT\),想當然地、草率地理解了理解,就開始打板子,對\(LCT\)一直是極為膚淺的認識。直到開始寫,才發現自己哪個地方都不會,理解的半生不熟,總之很慘……

寫部落格真是一個陶冶情操的過程啊……包括做表情包

加油吧,\(pks\)

\(\rm{Reference}\)

\(\mathfrak{writter:pks}\)