淺談Link-Cut Tree(LCT)
0XFF 前言&概念
Link-Cut Tree 是一種用來維護動態森林連通性的資料結構,適用於動態樹問題。它採用類似樹鏈剖分的輕重邊路徑剖分,把樹邊分為實邊和虛邊,並用 Splay 來維護每一條實路徑。Link-Cut Tree 的基本操作複雜度為均攤O(logn),但常數因子較大,一般效率會低於樹鏈剖分。但是卻可以解決樹鏈剖分解決不了的問題(或者優化碼量) -----Menci dalao
動態樹LCT(link cut tree)是一個可以動態維護森林上各種資訊的東西(刪除查詢合併啥的都有吧),原來的森林我們稱為原森林,裡面有實邊和虛邊,為啥有這兩種邊呢,首先LCT是用很多個splay維護這個森林的資訊,那麼因為splay本來就是個二叉樹,所以我們要將原森林”剖分”成很多個二叉樹並且用splay來維護它,用實邊連線起來的一棵樹就是原森林中的一棵樹,我們稱它為原樹。
這個Splay會有些特殊,它的關鍵字是節點在樹裡面的深度。
這棵原樹我們也不是直接用splay維護,而是按每個點在原樹中的深度為優先順序,將每個點以優先順序的中序遍歷丟到splay上。我們一般將原樹所對應的splay稱為輔助樹,原森林就對應一個輔助樹森林。
-----quhengyi11 dalao
請務必先將上文讀清楚,再繼續下面的閱讀。
Splay是輔助樹,閱讀時不要將主的和輔的搞混了。
顯然原樹中同一個深度的點是不可能在一個splay裡的,因此每個splay裡面就是維護了原樹中的一條鏈
Link-Cut Tree 準確的說是一個 Splay 森林。每棵 Splay
那麼現在每個結點都是一顆 Splay。
就像這樣:
如果我們將1,2連線起來的話。
那麼1,2就是同一個 Splay 中的節點了。
那麼現在的情況就是這樣:
相信你一定對此有些瞭解了吧。
0X01 一些基本的定義
f[x]
:結點x的爸爸(father)
v[x]
:結點x的權值(value)
s[x]
:結點x及它的子樹的權值和(sum)
r[x]
:結點x的翻轉情況(rev)
ch[x][0/1]
:結點x的左/右兒子
0X02 一些操作
Link-Cut Tree 支援以下幾種基本操作:
Access(x)
:將x到根節點的路徑上全部變成實邊,並棄掉自己所有的兒子(變成虛邊:認父不認子)(每一個父結點對於自己的每個子結點只有一條實邊)
findroot(x)
:找出x所在的原樹的根結點(實際上就是上圖的一號點)
makeroot(x)
:這個操作的意思是將x點變為原樹的根節點
split(x,y)
:將x,y搞在一個 Splay 中,以方便操作。
link(x,y)
:將x和y所在原樹合併起來(連結)
cut(x,y)
:將x和y所在原樹拆開(切斷)
Access(x):
這是最基礎的操作,意思是將點x到原樹中根結點root之間的鏈丟到一個輔助樹splay裡面
比方說,現在森林的狀態是這樣的:
我們的 x 現在等於6。執行 Access(6) 。
那麼就會將{1-3,3-6}變成實邊,1-2變成虛邊,假設6有一兒子n,之間用實邊連著,那麼這條邊也將變成虛邊。
每次將$x$點 splay 到當前所在輔助樹的根節點,將它的右兒子更新為上一個$x$,然後令$x$跳到它的父節點,特別的,第一個$x$的右兒子設為0(NULL)。
Q:為什麼是右兒子而不是左兒子呢?
A:因為f[x]的深度小於x,而在Splay裡面f[x]是x的爸爸,所以x在Splay中是f[x]的右兒子。
所以就變成了這樣:
我們將$x$旋轉到輔助樹的根節點,也就是將當前原樹這條鏈上深度小於$x$(在$x$上面的點)丟到了$x$的左子樹上,將$x$的右子樹設為上一個$x$點相當於將$x$原來的右子樹丟到了新的 splay 裡面(而它們之間用虛邊相連),並且將上一段鏈連線起來。
現在就可以了。這棵新 Splay 中只有這條鏈上的結點,沒有其他任何的結點。如果我們指定要這三個結點同時進行操作,可以直接下傳懶標記到這三個結點組成的 Splay 的根結點哦!到後面Splay的時候就可以直接下傳跟新結點資訊了。
總體過程:
虛邊:兒子認父,父不認子
實邊:兒子認父,父也認子
用FlashHu大佬的話來說,就是四步:
1.轉到根。
2.換兒子。
3.更新資訊。
4.當前操作點切換為輕邊所指的父親,轉1。
程式碼實現:
inline void Access(int x){
for(register int y=0;x;y=x,x=f[x]){
Splay(x);//轉到所在Splay的根節點
ch[x][1]=y;//認兒子了
pushup(x);//兒子有變化,更新
}
}
findroot(x):
- 首先要明白:
- 根節點是的深度最小的
我們可以通過x向上找,用 Access 操作可以將x和x的根結點搞到一個 Splay 裡。
又因為有BST的性質:x的左子樹所有結點的權值 < x < x右子樹所有結點的權值。
而我們又知道,在執行完 Access 操作後,這課 Splay 裡面的結點權值最大的(深度最大的)就是x。
於是我們可以將x Splay 到這棵 Splay 的根結點,那麼現在最左邊的節點便是這課樹的根結點了。
程式碼實現:
inline int findroot(int x){
Access(x);//Access將x和根結點搞到同一個Splay中
Splay(x);//轉到Splay的根結點
while(ch[x][0])pushdown(x),x=ch[x][0];//不斷的找左兒子&更新節點資訊
return x;//最左邊的就是根結點了。
}
makeroot(x):
將x到根結點的路徑上的點全部翻轉(即x變成了根節點)
具體操作是我們先將x點與原樹中的根打通一條鏈,那麼現在它們就在同一棵輔助樹裡面了,我們發現x一定是在它所在的輔助樹的中序遍歷的最後一個的(因為它是這條鏈上最深的點),我們把x點 splay 到輔助樹的根上,那麼x顯然是沒有右子樹的,我們要實現將x移到原樹的根上,也就是將x到根的這條鏈的深度全部翻轉一遍,在輔助樹上的體現就是將整棵樹翻轉一遍,我們可以寫個翻轉標記來減少複雜度。
程式碼實現:
inline void filp(int x){//Splay普通區間翻轉
swap(ch[x][0],ch[x][1]);r[x]^=1;
}
inline void makeroot(int x){
Access(x);
Splay(x);
filp(x);//懶標記&翻轉區間
}
split(x,y)
這個操作是將x到y之間的那條路徑丟到一棵輔助樹裡,並且這棵輔助樹以y節點為根(方便處理資訊)。
Splay 維護的是原樹中的一條鏈,我們不能保證x,y會在同一條鏈裡。
所以我們可以先把x變成原樹的根節點(這下子Access(y)就會將x到y之間的所有節點丟到一個 Splay 中了)。
最後如上面所講的,最後來一個 Splay(y) 就大功告成了。
程式碼實現:
inline void split(int x,int y){
makeroot(x);Access(y);Splay(y);
}
link(x,y):
將x和y所在原樹合併起來(連結)
首先將x點丟到原樹的根,然後去找找y的根是不是x,如果不是說明x,y不在一個原樹內,我們將x的父節點設為y,也就相當於從y到x連了一條虛邊。
程式碼實現:
inline void link(int x,int y){
makeroot(x);//丟到根
if(findroot(y)!=x)f[x]=y;//連結一條虛邊
//注意因為是虛邊,所以不能認兒子
}
cut(x,y):
首先我們先把x,y之間的那條邊用split(x,y)拎出來,因為x,y是相鄰的,所以y的左兒子一定是x,將它們的父子關係消滅掉即可。
消滅父子關係時一定滿足以下條件:
1.x和y在一個原樹裡(不在一個樹裡面往哪兒切啊)
2.split之後x是y的左兒子
3.x的右兒子是空的(保證了中序遍歷中y緊跟在x的後面,即深度相鄰)(x的權值(深度)只比y小1,而x又正好是直接連著y的,所以我們無法再找到 >x 而又 <y 的整數了)
程式碼實現:
inline void cut(int x,int y){
split(x,y);
if(findroot(y)==x&&f[x]==y&&!ch[x][1]){//判斷各種條件
f[x]=ch[y][0]=0;//徹底切斷關係
pushup(y);//兒子變了,更新
}return;
}
0X03 Splay的改動:
旋轉的改動:
這裡需要注意一下,如果x的父親節點的父親節點y已經不在當前的這棵輔助樹上,只需要連單向邊(也就是虛邊,認父不認子),否則正常連就行,這裡要和普通的rotate區分開來。
做個對比:
現在的rotate(x):
inline void rotate(int x){
int y=f[x],z=f[y],k=chk(x),v=ch[x][!k];
if(get(y))ch[z][chk(y)]=x;ch[x][!k]=y,ch[y][k]=v;
if(v)f[v]=y;f[y]=x,f[x]=z;pushup(y),pushup(x);
}
普通的rotate(x):
inline void rotate(int x){
int y=f[x],z=f[y],k=chk(x),v=ch[x][!k];
ch[z][chk(y)]=x;ch[x][!k]=y,ch[y][k]=v;
f[v]=y;f[y]=x,f[x]=z;pushup(y),pushup(x);
}
Splay的改動
同樣要注意一下只能Splay到輔助樹的根節點,Splay之前需先下傳一下這一條鏈上需操作的所有的點,用棧來完成即可,可以手寫棧來減少常數。
inline void Splay(int x){
int y=x,top=0;hep[++top]=y;
while(get(y))hep[++top]=y=f[y];
while(top)pushdown(hep[top--]);
while(get(x)){//基本普通的Splay
y=f[x],top=f[y];
if(get(y))
rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
rotate(x);
}pushup(x);return;
}
0X04 一些題目程式碼:
luogu P3690【模板】Link Cut Tree(動態樹)(模板題)
就是上文講的。
Code:
#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
#define RI register int
#define A printf("A")
#define C printf(" ")
using namespace std;
const int N=3e5+2;
template <typename Tp> inline void IN(Tp &x){
int f=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9')if(ch=='-')f=-1,ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();x*=f;
}int f[N],v[N],s[N],r[N],hep[N],ch[N][2];
inline int get(int x){
return ch[f[x]][0]==x||ch[f[x]][1]==x;
}
inline void pushup(int x){
s[x]=s[ch[x][1]]^s[ch[x][0]]^v[x];
}
inline void filp(int x){
swap(ch[x][0],ch[x][1]);r[x]^=1;
}
inline void pushdown(int x){
if(!r[x])return;r[x]=0;
if(ch[x][0])filp(ch[x][0]);
if(ch[x][1])filp(ch[x][1]);
}
inline void rotate(int x){
int y=f[x],z=f[y],k=ch[y][1]==x,v=ch[x][!k];
if(get(y))ch[z][ch[z][1]==y]=x;ch[x][!k]=y,ch[y][k]=v;
if(v)f[v]=y;f[y]=x,f[x]=z;pushup(y);
}
inline void Splay(int x){
int y=x,top=0;hep[++top]=y;
while(get(y))hep[++top]=y=f[y];
while(top)pushdown(hep[top--]);
while(get(x)){
y=f[x],top=f[y];
if(get(y))
rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
rotate(x);
}pushup(x);return;
}
inline void Access(int x){
for(register int y=0;x;x=f[y=x])
Splay(x),ch[x][1]=y,pushup(x);
}
inline void makeroot(int x){
Access(x);Splay(x);filp(x);
}
inline int findroot(int x){
Access(x);Splay(x);
while(ch[x][0])pushdown(x),x=ch[x][0];
return x;
}
inline void split(int x,int y){
makeroot(x);Access(y);Splay(y);
}
inline void link(int x,int y){
makeroot(x);if(findroot(y)!=x)f[x]=y;
}
inline void cut(int x,int y){
makeroot(x);
if(findroot(y)==x&&f[x]==y&&!ch[x][1]){
f[x]=ch[y][0]=0;pushup(y);
}return;
}int n,m,x,y,op;
int main(){
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;++i)scanf("%d",&v[i]);
for(register int i=1;i<=m;++i){
scanf("%d%d%d",&op,&x,&y);
if(op==0)split(x,y),printf("%d\n",s[y]);
else if(op==1)link(x,y);
else if(op==2)cut(x,y);
else Splay(x),v[x]=y;
}return 0;
}
[SDOI2008]洞穴勘測
分析:題目只要求link(有一條新道路==連線)和cut(道路被摧毀了==cut)以及判斷連通性(直接findroot,一樣的話那麼就是聯通的)
就是LCT的板子,真的沒那麼難。
Code:
#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
#define RI register int
#define A printf("A")
#define C printf(" ")
using namespace std;
const int N=2e5+2;
template <typename Tp> inline void IN(Tp &x){
int f=1;x=0;char ch=getchar();
while(ch<'0'||ch>'9')if(ch=='-')f=-1,ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();x*=f;
}int n,m,f[N],r[N],hep[N],ch[N][2];
inline int get(int x){return ch[f[x]][0]==x||ch[f[x]][1]==x;}
inline void filp(int x){swap(ch[x][0],ch[x][1]);r[x]^=1;}
inline void pushdown(int x){
if(!r[x])return;r[x]=0;
if(ch[x][0])filp(ch[x][0]);
if(ch[x][1])filp(ch[x][1]);
}
inline void rotate(int x){
int y=f[x],z=f[y],k=ch[y][1]==x,v=ch[x][!k];
if(get(y))ch[z][ch[z][1]==y]=x;ch[x][!k]=y,ch[y][k]=v;
if(v)f[v]=y;f[y]=x,f[x]=z;return;
}
inline void Splay(int x){
int y=x,top=0;hep[++top]=y;
while(get(y))hep[++top]=y=f[y];
while(top)pushdown(hep[top--]);
while(get(x)){
y=f[x],top=f[y];
if(get(y))
rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
rotate(x);
}return;
}
inline void Access(int x){
for(register int y=0;x;x=f[y=x])
Splay(x),ch[x][1]=y;
}
inline void makeroot(int x){
Access(x);Splay(x);filp(x);
}
inline int findroot(int x){
Access(x);Splay(x);
while(ch[x][0])pushdown(x),x=ch[x][0];
return x;
}
inline void split(int x,int y){
makeroot(x);Access(y);Splay(y);
}
inline void link(int x,int y){
makeroot(x);if(findroot(y)!=x)f[x]=y;
}
inline void cut(int x,int y){
makeroot(x);
if(findroot(y)==x&&f[x]==y&&!ch[x][1]){
f[x]=ch[y][0]=0;
}return;
}char op[16];
int main(){
scanf("%d%d",&n,&m);
for(register int x,y,i=1;i<=m;++i){
scanf("%s%d%d",op,&x,&y);
if(op[0]=='C')link(x,y);
else if(op[0]=='D')cut(x,y);
else if(op[0]=='Q'){
if(findroot(x)==findroot(y))printf("Yes\n");
else printf("No\n");
}
}return 0;
}
再推存一道題目:P1501 [國家集訓隊]Tree II
這道題目主要就是懶標記的運用,建議在做這一道題之前先去做一做線段樹的模板2,其實道理差不多,相通的,並不難。(講乘法標記的正確下傳方法弄到Splay的下傳上即可)
當然這道題我也附上題解:題解 P1501【[國家集訓隊]Tree II】(Link-Cut-Tree)
0X05 致謝:
感謝FlashHu大佬的文章:LCT總結
感謝Menci大佬的文章:Link-Cut Tree 學習筆記
感謝學長quhengyi11的文章:LCT學習筆記
感謝tgop_knight大佬的文章:link-cut tree
感謝露迭月大佬的文章:Link Cat Tree(連喵樹)學習筆記
鳴謝洛谷圖床,方便我上傳手繪圖片!
最後推薦一個網址,這裡面有一些基本的LCT例題以及作者的解析:傳送門