(持續更新)C++ LCT(Link-cut-tree) 動態樹 總結
準備知識:樹剖&Splay
一、理解LCT的工作原理
先看一道例題:
讓你維護一棵給定的樹,需要支持下面兩種操作:
Change x val: 令x點的點權變為val
Query x y: 計算x,y之間的唯一的最短路徑的點權的xor和
這是一道樹剖裸題。我們知道,當題目中出現了維護與樹上最短路相關的區間操作時,基本可以確定要用樹剖來做了。
再來看一下這道題的升級版:
讓你維護一棵給定的樹,需要支持下面四種操作:
Change x val: 令x點的點權變為val
Query x y: 計算x,y之間的唯一的最短路徑的點權xor和
Cut x y: 如果x,y間有邊相連,則刪除它。
Link x y: 如果x,y不聯通,則建立一條x,y之間的有向邊。
在這道題裏,我們增加了兩個操作,Link和Cut。我們發現,這道題不可以用樹剖來做了——顯然,樹剖無法處理修改樹的形狀的相關操作的。
現在我們就需要LCT了。
LCT,全稱Link Cut Tree,中文名“動態樹”。顧名思義,這種數據結構就是支持連邊和斷邊操作同時像樹剖那樣維護一些數據的樹。由於需要支持連邊斷邊,LCT就不能像樹鏈剖分一樣用線段樹來維護了,而需要使用更加靈活的延展樹(Splay)。
因此,與樹鏈剖分一樣,LCT需要滿足以下這些性質:
1.每一個Splay維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷Splay得到的每個點的深度序列嚴格遞增。
//只要把樹剖和Splay搞懂了,這一點不難理解。
2.每一個節點存在且只存在於一個Splay中
3.邊分為實邊和虛邊。實邊包含在Splay樹中中序遍歷的相鄰節點之間,而虛邊必須由一棵Splay指向另一個節點,也就是中序遍歷中最靠前的哪個節點的父親。
//這裏需要註意的是,一般LCT的虛邊是用Splay的根節點的father指針來實現的。
輔助理解
當一棵樹的虛實邊是這樣分配的時候,它所對應的伸展樹是這個樣子:
其中,每一個虛線框起來的區域都是一棵伸展樹;雙向箭頭指父節點存son,子節點存father的邊;單項箭頭指子節點存father,而父節點不存son的邊(虛邊)。
二、具體操作函數
1.access(int u) 具體功能:把節點u到根節點的路徑壓入一個Splay中(即標記為重邊)
這個沒圖的話實在難理解。仍然舉一個例子:
如果我們在上圖的情況下對節點M執行這一操作,那麽這棵樹就會有下圖的變化:
根據樣例我們可以看出,access的操作大概可以分兩步:
首先,Splay(M),將M旋轉到Splay的根節點;
然後,使節點M替代它的父親G的右子的位置,即G->rs=M。
這時,原本G的右子脫離了G所在的伸展樹,與G連接的邊變為了虛邊;而節點M對應的伸展樹則與G所在的伸展樹合並,M和G之間的虛邊變成了實邊。
循環執行這兩個操作,直到到達根節點,access操作就完成了。
這裏需要註意的是,當完成一次連實邊操作之後,需要將它的父親pushup,以保證其維護數據的正確性。
這樣,我們就得到了access的代碼:
inline void access(int u) { for(int v=0;u;v=u,u=father[u]) Splay(u), rs[u]=v, pushup(u); }access
2.makeroot(int u) 具體功能:令節點u成為其所在樹的根節點
當我們需要兩個點之間的路徑信息的時候,應該怎麽做?
如果其中一個點不是另一個點的祖先的話,它們是不可能在同一個Splay裏面的。
這時,我們就需要makeroot函數把其中一個點搞成根節點了。執行完access(u)操作之後,u會成為所在Splay深度最大的點;然後只需要把整棵樹翻轉,u就是整棵樹的根了。
代碼如下:
inline void makeroot(int u) { access(u); Splay(u); reverse(u); }makeroot
3.split(int u,int v) 具體功能:把節點u到節點v間的路徑壓入到一個Splay中
有了makeroot之後,split變得超級簡單:
inline void split(int u,int v) { makeroot(u); access(v); Splay(v); }split
4.findroot(int u) 具體功能:查詢該節點所在樹的根節點編號
主要應用於判斷連通性。
inline int findroot(int u) { access(u); Splay(u); pushdown(u); while(ls[u]) pushdown(u),u=ls[u]; return u; }findroot
5.link(int u,int v)
當u-v不連通時,連一條u到v的輕邊。
inline void link(int u,int v) { makeroot(u); if(findroot(v)!=u) father[u]=v; }link
6.cut(int u,int v)
根據LCT的性質,我們可以發現,當makeroot(u),access(v)之後,如果存在邊u-v,必須滿足以下條件:
(1)u-v聯通
(2)u-v在同一條Splay裏有父子關系
(3)u-v在Splay的中序遍歷中相鄰
這樣判斷是否有u-v的邏輯語句就完成了,直接刪除即可。
代碼如下:
inline void cut(int u,int v) { makeroot(u); if(findroot(v)==u && father[u]==v && rs[u]==0) father[u]=ls[v]=0,pushup(v); }cut
7. 伸展樹
這個伸展樹比一般Splay特殊一些。它的判根、rotate操作和Splay操作之前的pushall函數需要註意一下。
int val[MAXN],ls[MAXN],rs[MAXN],father[MAXN],orsum[MAXN],size[MAXN],rev[MAXN]; inline bool isroot(int u) { return ls[father[u]]!=u && rs[father[u]]!=u; } inline void pushup(int u) { orsum[u]=val[u]^orsum[ls[u]]^orsum[rs[u]]; size[u]=1+size[ls[u]]+size[rs[u]]; } inline void reverse(int u) { if(u) { swap(ls[u],rs[u]); rev[u]^=1; } } inline void pushdown(int u) { if(rev[u]) { reverse(ls[u]); reverse(rs[u]); rev[u]^=1; } } inline void rotate(int S) { //風格比較清奇,湊活看吧qaq int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; } father[S]=father[u]; father[u]=S; if(ls[u]==S) { if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u; } else { if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u; } pushup(u); pushup(S); } void pushall(int u) { if(!isroot(u)) { pushall(father[u]); pushdown(father[u]); } } inline void Splay(int u) { pushall(u); pushdown(u); //執行操作之前先pushdown.當然也可以手寫棧替代系統棧 while(!isroot(u)) { if(isroot(father[u])) rotate(u); else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u])) rotate(father[u]), rotate(u); else rotate(u), rotate(u); } }View Code
三、模板題
洛谷3690 【模板】Link Cut Tree
給定n個點以及每個點的權值,要你處理接下來的m個操作。操作有4種。操作從0到3編號。點從1到n編號。
0:後接兩個整數(x,y),代表詢問從x到y的路徑上的點的權值的xor和。保證x到y是聯通的。
1:後接兩個整數(x,y),代表連接x到y,若x到y已經聯通則無需連接。
2:後接兩個整數(x,y),代表刪除邊(x,y),不保證邊(x,y)存在。
3:後接兩個整數(x,y),代表將點x上的權值變成y。
這是一道裸模板題(其實也沒有什麽更復雜的操作了,大概就是這麽回事)
註意:該pushdown就pushdown;該pushup就pushup。
/* 716ms/4835K 多維護了一個size */ #include <iostream> #include <cstdio> using namespace std; const int MAXN=300006; int val[MAXN],ls[MAXN],rs[MAXN],father[MAXN],orsum[MAXN],size[MAXN],rev[MAXN]; inline bool isroot(int u) { return ls[father[u]]!=u && rs[father[u]]!=u; } inline void pushup(int u) { orsum[u]=val[u]^orsum[ls[u]]^orsum[rs[u]]; size[u]=1+size[ls[u]]+size[rs[u]]; } inline void reverse(int u) { if(u) { swap(ls[u],rs[u]); rev[u]^=1; } } inline void pushdown(int u) { if(rev[u]) { reverse(ls[u]); reverse(rs[u]); rev[u]^=1; } } inline void rotate(int S) { int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; } father[S]=father[u]; father[u]=S; if(ls[u]==S) { if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u; } else { if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u; } pushup(u); pushup(S); } void pushall(int u) { if(!isroot(u)) { pushall(father[u]); pushdown(father[u]); } } inline void Splay(int u) { pushall(u); pushdown(u); while(!isroot(u)) { if(isroot(father[u])) rotate(u); else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u])) rotate(father[u]), rotate(u); else rotate(u), rotate(u); } } inline void access(int u) { for(int v=0;u;v=u,u=father[u]) Splay(u), rs[u]=v, pushup(u); } inline void makeroot(int u) { access(u); Splay(u); reverse(u); } inline int findroot(int u) { access(u); Splay(u); pushdown(u); while(ls[u]) pushdown(u),u=ls[u]; return u; } inline void split(int u,int v) { makeroot(u); access(v); Splay(v); } inline void link(int u,int v) { makeroot(u); if(findroot(v)!=u) father[u]=v; } inline void cut(int u,int v) { makeroot(u); if(findroot(v)==u && father[u]==v && rs[u]==0) father[u]=ls[v]=0,pushup(v); } inline int read() { char ch=getchar(); int ret=0; while(ch<‘0‘ || ch>‘9‘) ch=getchar(); while(ch>=‘0‘ && ch<=‘9‘) ret=ret*10+(ch-‘0‘), ch=getchar(); return ret; } int main() { int n=read(),m=read(),opt,x,y; for(int i=1;i<=n;i++) val[i]=orsum[i]=read(),size[i]=1; while(m--) { opt=read(); x=read(); y=read(); if(opt==0) split(x,y), printf("%d %d\n",orsum[y],size[y]); else if(opt==1) link(x,y); else if(opt==2) cut(x,y); else access(x), Splay(x), val[x]=y, pushup(x); } }View Code
(持續更新)C++ LCT(Link-cut-tree) 動態樹 總結