\(\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}\)他居然:
他甚至還:
……
好吧,由於某些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}\)
- \([1]\) :\(Flash\_Hu\)的\(blog\) \(^{^{[\nearrow ]}}\)
- \([2]\) :某篇論文,結合食用效果顯著 \(^{^{[\nearrow]}}\)