前言
\(LCT\),真的是一個無比神奇的數據結構。
它可以動態維護鏈信息、連通性、邊權、子樹信息等各種神奇的東西。
而且,它其實並不難理解。
就算理解不了,它簡短的代碼也很好背。
\(LCT\)與實邊的定義
\(LCT\),全稱\(Link\ Cut\ Tree\),中文名動態樹。
它的實現有點類似於樹鏈剖分,但樹鏈剖分維護的是重邊和輕邊(故又稱重鏈剖分),而\(LCT\)維護的則是實邊和虛邊。
什么是實邊?
我們選擇一個節點與其一個兒子的連邊為實邊,與其他兒子的連邊為虛邊,這里的實邊是可以隨時變化的。
而實鏈剖分與樹鏈剖分最大的區別在於,樹鏈剖分是靜態的,所以可以用線段樹維護,而實鏈剖分則是動態的,因此就需要用一個更為神奇的數據結構——\(Splay\)來進行維護。
於是,就有了\(LCT\)這個奧秘重重的數據結構。
\(LCT\)的簡單性質
從上面的內容我們可以知道,\(LCT\)將一棵樹的邊分成了實邊和虛邊。
而連續的若干條實邊構成了實鏈。
而我們對每條實鏈分別用一個\(Splay\)進行維護,可以保證,每個\(Splay\)中維護的節點按中序遍歷得到的順序在原樹中深度依次增加\(1\)(證明:因為我們維護的是一條連續的鏈啊)。
而虛邊的作用則是將這些\(Splay\)給鏈接起來,大體連接方式如下:
- 找到該\(Splay\)中在原樹中深度最小的節點,記其為\(k\)。(具體代碼實現時是無需求出這個\(k\)的,這里只是方便理解)
- 如果\(k\)是原樹中的根節點,則無需連邊。
- 否則,我們找到\(fa_k\),將該\(Splay\)的根節點與\(fa_k\)之間連一條邊。
這樣一來,就把所有\(Splay\)連在了一起。
注意到一個節點可能有多個兒子,但實際上它只存儲一個兒子,某大佬用一句很精辟的話對其進行了總結:認父不認子。
\(LCT\)的基本操作
下面,我們來介紹幾個\(LCT\)的基本操作。
-
\(IsRoot(x)\)
\(IsRoot(x)\)的作用是判斷一個節點\(x\)是否是當前實樹的根。
由於我們知道\(LCT\)是認父不認子的,所以只需要判斷當前節點的父親節點的兩個子節點是否為當前節點即可。
代碼如下:
inline bool IsRoot(int x)//判斷一個節點x是否是當前實樹的根
{
return node[node[x].Father].Son[0]^x&&node[node[x].Father].Son[1]^x;//判斷fa[x]的兩個子節點是否為x
}
-
\(Rotate(x)\&\&Splay(x)\)
關於這個可以自行參考簡析平衡樹(三)——淺談Splay。
然而\(Splay\)和\(LCT\)中這兩個操作其實還是有一定區別的。
比如說,\(LCT\)每次固定將節點旋到根,因此只需要一個參數(雖然我博客中\(Splay\)的第一個模板也是只傳一個參的)。
再比如,\(LCT\)在\(Splay\)前需要先將當前節點到根節點的路徑上所有節點從上往下\(PushDown()\)一遍。這可以函數遞歸,也可以直接棧模擬。
具體代碼如下:
#define Which(x) (node[node[x].Father].Son[1]==x)
#define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
inline void Rotate(int x)
{
register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
!IsRoot(fa)&&(node[pa].Son[Which(fa)]=x),node[x].Father=pa,Connect(node[x].Son[d^1],fa,d),Connect(fa,x,d^1),PushUp(fa),PushUp(x);
}
inline void Splay(int x)
{
register int fa=x,Top=0;
while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;//存入棧中
while(Top) PushDown(Stack[Top]),--Top;//依次PushDown
while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
}
-
\(Access(x)\)
\(Access(x)\)的作用是把根節點到\(x\)的路徑上的邊全部變為實邊。
則我們首先考慮在當前\(Splay\)中將\(x\)旋到根,然后將\(x\)與\(fa_x\)間的連邊更新為實邊,即更新\(fa_x\)的右兒子為\(x\);再將\(fa_x\)在其所在\(Splay\)中旋到根,同理更新\(fa_{fa_x}\)的右兒子為\(fa_x\)... ...
以此類推,直到處理到根節點所在的\(Splay\)為止。
這樣就打通了一條從根節點到\(x\)的路徑。
\(Access(x)\)可謂是\(LCT\)最核心的操作,也是后面許多操作的基礎。
具體實現可以詳見代碼:
inline void Access(int x)//把根節點到x的路徑上的邊全部變為實邊
{
for(register int son=0;x;x=node[son=x].Father)
Splay(x),node[x].Son[1]=son,PushUp(x);//注意Access過程中要PushUp
}
-
\(FindRoot(x)\)
\(FindRoot(x)\)的作用是找到\(x\)所在的原樹中的根節點,可以用來判斷連通性,實現可撤銷並查集。
我們首先\(Access(x)\)打通一條從根到\(x\)的路徑,此時\(x\)就與根節點在同一個\(Splay\)內了。
然后\(Splay(x)\)將\(x\)旋到根。
記住前面提到的\(LCT\)的性質:每個\(Splay\)中維護的節點按中序遍歷得到的順序在原樹中深度依次增加\(1\)。
所以根節點必然是\(Splay\)中中序遍歷順序為\(1\)的節點。
而這其實就是\(x\)盡量向左兒子拓展最后得到的節點。
代碼如下:
inline int FindRoot(int x)
{
Access(x),Splay(x);//一波操作,將x轉到根節點所在Splay的根
while(node[x].Son[0]) PushDown(x),x=node[x].Son[0];//盡量向左兒子拓展,注意每次拓展前先PushDown
return Splay(x),x;//最后不忘Splay的優良傳統:每執行完一個操作就Splay一下,防被卡
}
-
\(MakeRoot(x)\)
\(MakeRoot(x)\)的作用是將\(x\)作為原樹中的新的根節點。
首先,依然是先\(Access(x)\)打通一條從根到\(x\)的路徑,然后\(Splay(x)\)將\(x\)旋到根。
由前面的操作可知,根節點是\(Splay\)中中序遍歷順序為\(1\)的節點。
而此時,\(x\)必然是\(Splay\)中中序遍歷最后得到的點。
因此我們只要翻轉該\(Splay\),\(x\)就變成中序遍歷順序為\(1\)的節點了。
代碼如下:
inline void Rever(int x)//翻轉子樹
{
swap(node[x].Son[0],node[x].Son[1]),node[x].Rev^=1;//交換左右兒子,然后更新標記
}
inline void MakeRoot(int x)//將x作為原樹中的新的根節點
{
Access(x),Splay(x),Rever(x);//將x轉到根節點所在Splay的根,然后翻轉Splay
}
-
\(Link(x,y)\)
\(Link(x,y)\)的作用是在\(x\)和\(y\)兩個節點間連一條邊。
首先,我們將\(x\)作為它所在樹的根,即\(MakeRoot(x)\)。
然后,我們需要判斷\(x\)與\(y\)是否聯通。由於\(x\)是其所在子樹的根節點,因此只要判斷\(FindRoot(y)\)是否為\(x\)即可。
連接只需要更新\(x\)的父親為\(y\)即可。
代碼如下:
inline void Link(int x,int y)//在x和y兩個節點間連一條邊
{
MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);//判斷x和y的連通性,然后連接
}
-
\(Cut(x,y)\)
\(Cut(x,y)\)的作用是刪除\(x\)和\(y\)之間的邊。
首先,我們依然將\(x\)作為它所在樹的根。
則可以保證,若\(x\)和\(y\)有邊相連,一定滿足一下三個條件:
- \(y\)所在樹的根為\(x\),即\(FindRoot(y)==x\)。
- \(y\)的父親節點為\(x\),即\(fa_y==x\)。
- \(y\)沒有左兒子。因為如果\(y\)有左兒子,由於\(LCT\)的性質,可得\(Depth_x<Depth_{leftson_y}<Depth_y\),則\(x\)和\(y\)必然不相連。
代碼如下:
inline void Cut(int x,int y)//刪除x和y之間的邊
{
MakeRoot(x),!(FindRoot(y)^x)&&!(node[y].Father^x)&&!node[y].Son[0]&&(node[y].Father=node[x].Son[1]=0,PushUp(x));//判斷x和y的連通性,然后刪邊
}
-
\(Split(x,y)\)
\(Split(x,y)\)的作用是從\(LCT\)中摳出\(x\)與\(y\)之間的路徑。
這樣一來,就方便我們查詢了。
這個操作第一步便是將\(x\)作為根,然后打通\(x\)到\(y\)的路徑。
可以保證,此時\(x\)與\(y\)所在的\(Splay\)內只包含\(x\)與\(y\)路徑上的節點。
然后我們將\(Splay(y)\)將\(y\)旋至\(Splay\)的根,這樣一來就可以通過查詢\(y\)的信息來進行詢問了。
代碼如下:
inline void Split(int x,int y)//從LCT中摳出x與y之間的路徑
{
MakeRoot(x),Access(y),Splay(y);//將x作為根,打通x與y的路徑並將y旋到根
}
模板(板子題)
#include<bits/stdc++.h>
#define N 300000
#define swap(x,y) (x^=y^=x^=y)
using namespace std;
int n,a[N+5];
class Class_FIO
{
private:
#define Fsize 100000
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,Fsize,stdin),A==B)?EOF:*A++)
#define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,Fsize,stdout),Fout[(FoutSize=0)++]=ch))
int Top,FoutSize;char ch,*A,*B,Fin[Fsize],Fout[Fsize],Stack[Fsize];
public:
Class_FIO() {A=B=Fin;}
inline void read(int &x) {x=0;while(!isdigit(ch=tc()));while(x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));}
inline void writeln(int x) {while(Stack[++Top]=x%10+48,x/=10);while(Top) pc(Stack[Top--]);pc('\n');}
inline void clear() {fwrite(Fout,1,FoutSize,stdout),FoutSize=0;}
}F;
class Class_LCT
{
private:
#define LCT_SIZE N
#define PushUp(x) (node[x].Sum=node[x].Val^node[node[x].Son[0]].Sum^node[node[x].Son[1]].Sum)
#define Rever(x) (swap(node[x].Son[0],node[x].Son[1]),node[x].Rev^=1)
#define PushDown(x) (node[x].Rev&&(Rever(node[x].Son[0]),Rever(node[x].Son[1]),node[x].Rev=0))
#define Which(x) (node[node[x].Father].Son[1]==x)
#define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
#define IsRoot(x) (node[node[x].Father].Son[0]^x&&node[node[x].Father].Son[1]^x)
#define MakeRoot(x) (Access(x),Splay(x),Rever(x))
#define Split(x,y) (MakeRoot(x),Access(y),Splay(y))
int Stack[LCT_SIZE+5];
struct Tree
{
int Val,Sum,Father,Rev,Son[2];
}node[LCT_SIZE+5];
inline void Rotate(int x)
{
register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
!IsRoot(fa)&&(node[pa].Son[Which(fa)]=x),node[x].Father=pa,Connect(node[x].Son[d^1],fa,d),Connect(fa,x,d^1),PushUp(fa),PushUp(x);
}
inline void Splay(int x)
{
register int fa=x,Top=0;
while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;
while(Top) PushDown(Stack[Top]),--Top;
while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
}
inline void Access(int x) {for(register int son=0;x;x=node[son=x].Father) Splay(x),node[x].Son[1]=son,PushUp(x);}
inline int FindRoot(int x) {Access(x),Splay(x);while(node[x].Son[0]) PushDown(x),x=node[x].Son[0];return Splay(x),x;}
public:
inline void Init(int len,int *data) {for(register int i=1;i<=len;++i) node[i].Val=data[i];}
inline void Link(int x,int y) {MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);}
inline void Cut(int x,int y) {MakeRoot(x),!(FindRoot(y)^x)&&!(node[y].Father^x)&&!node[y].Son[0]&&(node[y].Father=node[x].Son[1]=0,PushUp(x));}
inline void Update(int x,int v) {Splay(x),node[x].Val=v;}
inline int Query(int x,int y) {return Split(x,y),node[y].Sum;}
}LCT;
int main()
{
register int query_tot,i,op,x,y;
for(F.read(n),F.read(query_tot),i=1;i<=n;++i) F.read(a[i]);
for(LCT.Init(n,a);query_tot;--query_tot)
{
F.read(op),F.read(x),F.read(y);
switch(op)
{
case 0:F.writeln(LCT.Query(x,y));break;
case 1:LCT.Link(x,y);break;
case 2:LCT.Cut(x,y);break;
case 3:LCT.Update(x,y);break;
}
}
return F.clear(),0;
}
后記
推薦一些比較好的\(LCT\)題目:【轉載】LCT題單。