前言:
Link-Cut Tree簡稱LCT是解決動態樹問題的一種數據結構,可以說是我見過功能最強大的一種樹上數據結構了。在此與大家分享一下LCT的學習筆記。提示:前置知識點需要樹鏈剖分和splay。
引例:
在講LCT之前先來看一道題:給一棵樹,每個點有一個點權,多次操作,操作包含1、修改路徑上點權2、查詢路徑上點權和。這道題顯然用樹鏈剖分+線段樹就能做,但現在再加兩個操作:3、刪除樹上的一條邊4、連接兩個點,保證連接后的聯通塊是一棵樹。樹的形態發生了改變,樹鏈剖分+線段樹這種靜態數據結構顯然做不了,我們要應用一些動態的數據結構來維護樹上信息——平衡樹(因為是區間操作所以采用splay,當然也可以用非旋轉treap,不過比較麻煩),那么能否沿用樹鏈剖分來解決呢?答案是可以的,但並不是上述的樹鏈剖分方法而是沿用樹鏈剖分的思想。因此可以將LCT看做是樹鏈剖分+splay。
LCT的構建:
對於一棵樹上的一個點,我們依舊選出它的一個子節點作為它的重兒子(這里的重兒子不是根據子樹大小決定的),每個點與重兒子之間的邊為重邊,重邊連成的鏈為重鏈。因為是動態樹,所以重兒子是可變的。對於每一條重鏈,我們用一棵splay來維護鏈的信息,splay的key值為每個點的深度。每棵splay的根節點指向這棵splay維護的鏈的鏈頭的父節點,這里注意是單方向指向,splay根節點能找到指向的鏈頭父節點,但從這個父節點找不到這棵splay的根節點。通俗點說就是父親不認兒子,兒子認父親。我們將這些splay組成的樹成為輔助樹。總的來說,對於原樹的一個節點:和它的重兒子在同一棵splay中,被它的輕兒子所在splay的根節點指向。注意輔助樹與原樹的結構並不相同。如下圖所示,無向邊是splay中的邊,有向邊是上述說的兒子認父親的指向邊。
LCT的基本操作:
先聲明一下變量:s[x][0/1]代表x的左右子節點,f[x]表示x的父節點,st[]代表splay時用到的棧,r[x]代表旋轉標記
is_root
用途:判斷一個點是否是它所在的splay的根。
實現:因為splay的根與其父親之間是單向指向的邊,所以只要判斷它的父節點的左右子節點都不是它就好了。
補充:LCT中有許多splay,因此判斷splay根的操作與通常splay略有不同。
int is_root(int rt) { return s[f[rt]][0]!=rt&&s[f[rt]][1]!=rt; }
splay
用途:將一個點旋到它所在splay的根。
實現:先將這個點到splay的根路徑上的點都記錄下來,從上往下下傳標記后按正常splay那樣旋到根即可。
補充:因為后續有一個操作需要區間翻轉,而在splay之前可能在所旋點到根路徑上還存有標記。
void splay(int rt) { int top=0; st[++top]=rt; for(int i=rt;!is_root(i);i=f[i]) { st[++top]=f[i]; } for(int i=top;i>=1;i--) { pushdown(st[i]); } for(int fa;!is_root(rt);rotate(rt)) { if(!is_root(fa=f[rt])) { rotate(get(rt)==get(fa)?fa:rt); } } }
access
用途:將原樹中一個點到根的路徑變成一條重鏈並將這個點與它子節點間的重鏈斷開。也就是將這個點到根路徑上的所有點放到同一個splay中。
實現:假設要操作點是x,那么x一定是這條到根路徑上深度最深的,將x的右子樹設為0即切斷了與子節點間的鏈(因為右子樹中的點都是深度比他大的),再將x旋到當前splay的根處,然后將x跳到它的父節點(也就是它指向的節點),重復上述操作,但要記錄上一次splay的節點,每次splay之后,將當前splay的節點的右兒子設為上次splay的節點。
補充:這是LCT中最重要的操作之一,也是查詢路徑信息時所必須的一步操作。
void access(int rt) { for(int x=0;rt;x=rt,rt=f[rt]) { splay(rt); s[rt][1]=x; pushup(rt); } }
reverse
用途:將一個點旋成原樹中的根。
實現:假設操作點為x,先將原樹中x到根路徑變成一條鏈access(x),再將x旋到它所在splay的根splay(x),這時x沒有右兒子,它到原樹根路徑上的點都在它的左子樹中,只要給x打一個旋轉標記,這樣它就沒有了左子樹,也就是沒有深度比它小的點,它就成為了根。
補充:當詢問路徑(x,y)上的信息時,通常是先將x旋到原樹的根reverse(x),再將y到根(也就是x)路徑變成一條鏈access(y),這時y所在splay中的所有節點就都是原樹x到y路徑上的節點,只要再把y旋到splay的根O(1)查詢即可。
void reverse(int rt) { access(rt); splay(rt); r[rt]^=1; }
link
用途:連接兩個點。
實現:例如連接x,y兩個點,先將x旋到原樹的根(因為只有這時x才沒有父親,可以指向),直接將它指向y即可。
補充:連接后只是x單向指向y。
void link(int x,int y) { reverse(x); f[x]=y; }
cut
用途:切斷兩個點之間的邊。
實現:例如切斷x,y兩個點之間的邊,先將x旋到原樹的根reverse(x),再將y到原樹根的路徑變為一條鏈access(y),然后將y旋到所在splay的根splay(y),因為x,y之間有邊,所以x一定是y的左子節點,將x的父親及y的左兒子置0即可。
補充:有些題不保證x,y之間有邊,因此要有一些特判(代碼中有實現)。
void cut(int x,int y) { reverse(x); access(y); splay(y); if(s[x][1]||f[x]!=y) { return ; } s[y][0]=f[x]=0; }
find
用途:找到一個點所在原樹的根節點。
實現:假設查找點為x,先將x到根路徑變成一條鏈access(x),再將x旋到splay的根splay(x),這時因為根節點深度最小,所以根在x所在splay中最左子樹中,直接一直找左子樹,直到當前點沒有左子節點為止,此時的點就是根。
補充:這個操作通常用於判斷兩個點的連通性。
int find(int rt) { access(rt); splay(rt); while(s[rt][0]) { rt=s[rt][0]; } return rt; }
LCT的時間復雜度:
觀察上述幾個操作發現除了access之外易證其他操作單次都是均攤O(logn)。那么探究LCT的時間復雜度就在於探究access操作的時間復雜度。因為一次access的路徑上指向的邊有logn條,所以也就有logn次splay操作,那么這些splay操作是均攤logn的。具體證明參考楊哲的論文《QTREE 解法的一些研究》。
LCT維護原樹子樹信息:
上面只講了LCT維護原樹路徑信息,那么LCT能否維護原樹子樹信息呢?答案是可以的。我們定義一個點的左右子節點為實兒子,指向它的點是它的虛兒子。那么原樹路徑信息就是實兒子子樹信息之和,而原樹子樹信息其實就是實兒子和虛兒子子樹信息之和。那么我們每個點維護兩個信息,一個是總兒子信息,也就是原樹中子樹信息,一個是虛兒子的信息,上傳直接像上述那樣合並就好了。但能發現虛兒子信息不是一直不變的,觀察在哪里改變了虛兒子信息。一個是在access時,另一個是在link時,access時每次往上爬都會將原來的右兒子變成虛兒子,將上次splay的點變成新的右兒子,這里要更新虛兒子信息,當然不管怎樣他們都是這個點的兒子,因此總兒子信息不變。link時會把x指向y,y會多一個虛兒子,因此要更新y的虛兒子信息。這里注意嚴格意義上一個點在原樹上的子樹信息只包含虛兒子子樹信息及實兒子中右兒子的子樹信息(因為左兒子子樹信息是這個點所在重鏈中比它深度淺的點的信息和),但因為我們每次查詢時都將查詢點變為原樹的根,所以這個點在LCT上不存在左兒子,因此可以像上述那樣維護信息。
以維護原樹子樹節點數為例,其中sum代表總兒子信息,size代表虛兒子信息。
access
void access(int rt) { for(int x=0;rt;x=rt,rt=f[rt]) { splay(rt); size[rt]+=sum[s[rt][1]]-sum[x]; s[rt][1]=x; pushup(rt); } }
link
void link(int x,int y) { reverse(x); reverse(y); f[x]=y; size[y]+=sum[x]; pushup(y); }
LCT維護原樹邊上信息:
通過上述講解可以發現LCT上的邊並不是原樹上的邊,那么如果題目要求維護原樹邊上信息該怎么做呢?我們將原樹上的邊在LCT上也建立一個點來維護這條邊的信息,例如:原樹上有一條邊為(x,y),我們新建一個點z來維護這條邊的信息,當原樹(x,y)這條邊被連接上時,原本在LCT上應該link(x,y),現在改為link(x,z)和link(z,y),同樣在刪邊時也要cut(x,z)和cut(z,y)。因為有刪邊操作,所以要記錄原樹每條邊的兩個端點。
TopTree:
上面說到了如何維護原樹子樹信息即維護LCT上輕兒子信息,那么如何修改原樹的子樹信息呢?因為一個點的輕兒子數量是不固定的,如果只是單純的記錄每個點的輕兒子並打標記下傳的話,那么就無法保證下傳的時間復雜度,所以我們引入了一種新的數據結構——TopTree。TopTree就是對於LCT上每個點,建立一個splay(即新建一些點組成一個splay),將splay的根作為這個點的輕兒子,而這個點原先所有的輕兒子則按一定順序連到splay的每個節點下面作為他們的輕兒子(輕兒子順序因題而異,且輕兒子順序決定了新建splay的key值),TopTree的結構如圖所示。
其中帶箭頭指向的是輕兒子,左邊是LCT,右邊是TopTree。1~6號節點為原樹點,7、8號節點為用來管理輕兒子的建立的splay上的點。可以發現整棵TopTree分為兩部分,維護一條重鏈的splay即原LCT上的splay和維護一個點輕兒子的splay即新建的splay。這樣在維護輕兒子的splay上移動即可實現在一個點的不同輕兒子間移動,同樣對於原樹子樹修改也可以通過下傳到維護輕兒子的splay中再進一步下傳到對應輕兒子所在重鏈的splay中來實現。因為LCT中的splay和TopTree中新建的splay作用不同,所以對於splay的所有操作都要在兩種splay中分別實現即寫兩種splay的操作,代碼量巨大且及其難調。因為每個點只有被當作輕兒子時才會在上面新建一個節點來維護他的信息,而每個點被當作輕兒子一次,所以新建節點數為$O(n)$。
LCT的練習題:
BZOJ2049[Sdoi2008]洞穴勘探(LCT模板題,只有link和cut)
BZOJ3282Tree(LCT模板題,單點修改,求路徑異或和)
BZOJ2631tree(LCT模板題,路徑加,路徑乘,求路徑點權和)
BZOJ2002[Hnoi2002]彈飛綿羊(LCT練習題,重點在於如何轉化成LCT)
BZOJ3669[Noi2014]魔法森林(LCT經典題,利用LCT解決二維最小生成樹)
BZOJ4530[Bjoi2014]大融合(LCT維護子樹信息)
BZOJ3091城市旅行(LCT區間信息合並)