[學習筆記]動態樹Link-Cut-Tree
參考&&推薦:
[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