1. 程式人生 > 其它 >Link-Cut Tree

Link-Cut Tree

實鏈剖分

就是可以隨意亂剖,想幹嘛幹嘛的剖分。我們面對動態樹問題(樹形會變化的問題),用實鏈剖分可以靈活的維護。然後因為實鏈剖分比較靈活,我們類比重鏈剖分,也考慮使用一種資料結構維護這個樹上資訊。論靈活度,相比線段樹來說,splay 是更好的選擇。

LCT

也叫 link-cut tree ,用於維護動態森林。具體來說可以被稱為一種輔助森林,裡面有很多輔助樹,每個輔助樹由很多 splay 樹組成,而每個 splay 維護樹上的一條實鏈。一般來說比較擅長維護路徑資訊,處理子樹的資訊似乎不太行?

而LCT有以下的一些性質:

1.具體考慮一下 splay 怎麼把實鏈分開?實際上就是對於有虛邊相連的兩條實鏈,它們的 splay 的關係是 兒子認父親,父親不認兒子

。那麼顯然,每個節點最多向其中一個兒子拉一條實鏈(暫且不考慮一個 splay 內部發生 splay 操作的情況下,也在就是原樹中)。

2.每個點只包含在 LCT 中的一個 splay 中。

3.LCT 中的 splay 保證中序遍歷下深度嚴格遞增。因為每條實鏈都是按照原樹剖分得到的結果。因此深度相同的點也不應當出現在同一棵 splay 中。

具體操作

splay基操

由於我們 LCT 的性質1,我們判斷一個點是否存在於當前 splay需要換一下,除此之外沒有任何區別。

我們判斷一個點是不是當前 splay 的根的方法並非看它是否擁有父節點,而是看其父節點是否認它。那麼我們判斷一個點是否存在於當前 splay 就是看它的兒子是否為當前 splay 的根;若為根,則不為 splay 中節點,反而反之。

別的都沒什麼區別,不多說了,直接上程式碼(可以看看一些細節變化):

inline void change(int x){//按修改
    std::swap(lc(x),rc(x) );
}
inline void pushdown(int x){//標記下傳
    if(t[x].rev){
        t[lc(x)].rev^=1;
        t[rc(x)].rev^=1;
        change(lc(x) );
        change(rc(x) );
        t[x].rev^=1;
    }
}
inline void pushup(int x){//向上維護資訊
    t[x].sz=(t[lc(x)].sz+t[rc(x)].sz)+1;
}
inline bool Get(int x){//判斷所屬兒子型別
    return x==rc(fa(x) );
}
inline bool isroot(int x){//判斷是否為根
    return lc(fa(x) )!=x and rc(fa(x) )!=x;
}
inline void rotate(int x){
    int y=fa(x),z=fa(y);bool ty=Get(x);
    if(!isroot(y) ) t[z].ch[Get(y)]=x;
    //由於isroot的判定方法,所以提前
    t[y].ch[ty]=t[x].ch[ty^1];
    if(t[x].ch[ty^1]) fa(t[x].ch[ty^1])=y;
    t[x].ch[ty^1]=y;
    fa(y)=x;fa(x)=z;
    pushup(y);pushup(x);
}
void update(int x){//提前下傳tag
    if(!isroot(x) ) update(fa(x) );
    pushdown(x);
}
inline void splay(int x){
    update(x);//先下傳好標記,不然旋轉的時候下傳顯然是錯的
    for(int f=fa(x);f=fa(x),!isroot(x);rotate(x) ){
        if(!isroot(f) ) rotate(Get(x)==Get(f)?f:x);
    }
}

比較需要注意的是我們的 change 函式不應該根據當前點是否具有翻轉標記來決定,因為我們是一傳了標記就給它換了,所以之後如果再次打了翻轉標記應當換回來。

access(連通)

核心函式之一。

我們使用這個操作,使得整個輔助樹內的根與當前點 \(x\) 直接開出一條實鏈(我們形象地稱之為 ✟昇天✟)

具體操作如何?我們先從 \(x\) 點開始,使之升至當前 splay 的根節點,並且斷開一條連向右兒子的邊。然後我們取到其父節點以跳到上方的 splay,然後斷右兒子,連向我們剛剛跳過來的地方。如此反覆直到到達根節點。

為什麼要這樣搞呢?因為當我們要新連出一條實邊,我們必然需要在原樹上斷開一條原先的實邊。而斷開右兒子的原因便是因為右兒子的深度由於性質3一定更大,而我們顯然不應該斷開一條連向原樹中父親的邊來換一條實邊,否則這直接就不是樹了。因此我們斷開右兒子來為下方的連通騰出位置。

inline int access(int x){
    int p;
    for(p=0;x;p=x,x=fa(x) ){
        splay(x);rc(x)=p;pushup(x);
    }
    return p;
}

你發現我這個函式有返回值,這個返回值是我們連通的過程中由虛變實的最後一個連線點,也就是最後一條虛變實的邊中更高的那一個點。我們看看這東西有沒有什麼性質。

我們發現在做完一次 access 的情況下,我們再做一次,那麼這個返回值是兩點的 LCA。因為在我們第一次已經打通了一條實鏈的情況下,其他點要去開根一點會要通過一條虛邊進入這條實鏈,而且經過之後不會再經過虛邊,所以可以得到 LCA。所以這個可以用來求 LCA。

而且顯然,在一個點做完 access 之後,就變成整個 splay 中中序遍歷最後遍歷的點。因為我們第一次就直接把它的右兒子斷開了。

makeroot(換根)

核心函式之二。

發現如果我們需要維護一條深度不單調遞增的路徑,由於性質3這條路徑的點不可能出現在同一棵 splay 下。

那我們怎麼辦!要寄了嗎?.jpg

因此這個函式存在的意義可知矣。這個操作能將一個節點換為當前的根。發現只需要連通 \(x\) 與根,並 splay \(x\) 即可。但是我們發現還有一點問題,這個時候的 \(x\) 僅僅是名義上坐在根的位置上,但是並沒有以他為根的性質。

我們發現如果我們把樹看成從父親向兒子的有向圖,在原樹上我們需要將根到 \(x\) 路徑上的邊方向反向。這啟發了我們,當我們做完 access 後,這個點變為中序遍歷最後遍歷的點。我們要使其變為根,就是使其變為中序遍歷最先遍歷的點,那麼我們直接給它打上翻轉標記,由於其沒有右子樹,只有左子樹,我們翻轉過後就使其正好成為中序遍歷最先遍歷的點了,達到目的。

inline void makeroot(int x){
    access(x);splay(x);
    t[x].rev^=1;change(x);
}

find(尋根)

找到 \(x\) 所在輔助樹的根。

直接 access(x) 然後 splay(x) 就可以得到 splay 樹最高的位置,然後一直跳左兒子找到深度最低的點即為答案。

別忘了找跳左兒子的時候傳標記已經最後將根旋到 splay 的根以保證複雜度。

inline int find(int x){
    access(x);splay(x);
    while(lc(x) ){
        pushdown(x);
        x=lc(x);
    }
    splay(x);
    return x;
}

link(連線)

直接拿出這個條邊端點的一個,把它 makeroot 然後直接把它父親接到另一個即可。

有的時候需要檢驗操作是否合法,用 find 即可。

inline void link(int x,int y){
    makeroot(x);
    if(find(y)==x) return;
    fa(x)=y;
}

split(抽離)

抽出維護路徑 \(x\)\(y\) 的splay。

\(x\) makeroot,然後 access \(y\) 即可。

inline void split(int x,int y){
    makeroot(x);access(y);splay(y);
}

\(y\) 提起來之後直接訪問 \(y\) 就可以訪問路徑資訊了。

注意最後這個 splay 不可省去,因為 \(y\) 還在老底下沒上來, \(x\) 也被 access 給創下去了。

cut(切割)

如果合法的話操作非常簡單,我們直接 split,此時 \(x\) 為原樹的根,\(y\) 為輔助樹的根,那麼 \(x\) 在輔助樹上肯定為 \(y\) 的左兒子。直接斷開。

inline void cut(int x,int y){
    split(x,y);fa(x)=lc(y)=0;
    pushup(y);
}

否則我們考慮怎麼判斷不合法。

我們首先把 \(x\) makeroot,然後用 find 判斷連通性,此時 \(x\) 被拉至根,我們要判斷他們直接是否有連邊,只需要滿足 \(x\)\(y\) 的父親,並且 \(y\) 沒有左子樹(這樣可知 \(x\)\(y\) 深度差 \(1\) )。最後更新一下資訊就好。

inline void cut(int x,int y){
    makeroot(x);
    if(find(y)!=x or fa(y)!=x or lc(y) ) return;
    fa(y)=rc(x)=0;
    pushup(x);
}

其實也可以:

inline void cut(int x,int y){
    makeroot(x);
    if(find(y)!=x or t[x].sz>2) return;
    fa(y)=rc(x)=0;
    pushup(x);
}

因為如果有邊相連,它們 access(y) 之後肯定只有這兩個點了。

主要操作就這些了。

(那差不多就沒啦?)

模板

參考程式碼

#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout);
#define gc getchar
#define pc putchar
namespace IO{
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=1e5+3;
struct LCT{
	#define lc(x) t[x].ch[0]
	#define rc(x) t[x].ch[1]
	#define fa(x) t[x].fa
	struct node{
		int fa,ch[2],val;
		int sz,sum;
		bool rev;
	}t[N];
	inline void change(int x){
		std::swap(lc(x),rc(x) );
	}
	inline void pushdown(int x){
		if(t[x].rev){
			t[lc(x)].rev^=1;
			t[rc(x)].rev^=1;
			change(lc(x) );
			change(rc(x) );
			t[x].rev=0;
		}
	}
	inline void pushup(int x){
		t[x].sz=(t[lc(x)].sz+t[rc(x)].sz)+1;
		t[x].sum=(t[lc(x)].sum^t[rc(x)].sum)^t[x].val;
	}
	inline bool Get(int x){
		return x==rc(fa(x) );
	}
	inline bool isroot(int x){
		return lc(fa(x) )!=x and rc(fa(x) )!=x;
	}
	inline void rotate(int x){
		int y=fa(x),z=fa(y);bool ty=Get(x);
		if(!isroot(y) ) t[z].ch[Get(y)]=x;
		//由於isroot的判定方法,所以提前
		t[y].ch[ty]=t[x].ch[ty^1];
		if(t[x].ch[ty^1]) fa(t[x].ch[ty^1])=y;
		t[x].ch[ty^1]=y;
		fa(y)=x;fa(x)=z;
		pushup(y);pushup(x);
	}
	void update(int x){
		if(!isroot(x) ) update(fa(x) );
		pushdown(x);
	}
	inline void splay(int x){
		update(x);
		for(int f=fa(x);f=fa(x),!isroot(x);rotate(x) ){
			if(!isroot(f) ) rotate(Get(x)==Get(f)?f:x);
		}
	}
	inline int access(int x){
		int p;
		for(p=0;x;p=x,x=fa(x) ){
			splay(x);rc(x)=p;pushup(x);
		}
		return p;
	}
	inline void makeroot(int x){
		access(x);splay(x);
		t[x].rev^=1;change(x);
	}
	inline int find(int x){
		access(x);splay(x);
		while(lc(x) ){
			pushdown(x);
			x=lc(x);
		}
		splay(x);
		return x;
	}
	inline void link(int x,int y){
		makeroot(x);
		if(find(y)==x) return;
		fa(x)=y;
	}
	inline void split(int x,int y){
		makeroot(x);access(y);splay(y);
	}
	inline void cut(int x,int y){
		makeroot(x);
		if(find(y)!=x or t[x].sz>2) return;
		fa(y)=rc(x)=0;
		pushup(x);
	}
	#undef lc
	#undef rc
	#undef fa
}s;
int main(){
	filein(a);fileot(a);
	int n,m;
	read(n,m);
	for(int i=1;i<=n;++i){
		read(s.t[i].val);
	}
	for(int i=1;i<=m;++i){
		int op;read(op);
		if(op==0){
			int x,y;
			read(x,y);
			s.split(x,y);
			printf("%d\n",s.t[y].sum);
		}else if(op==1){
			int x,y;
			read(x,y);
			s.link(x,y);
		}else if(op==2){
			int x,y;
			read(x,y);
			s.cut(x,y);
		}else{
			int x,y;
			read(x,y);
			s.splay(x);
			s.t[x].val=y;
			s.pushup(x);
		}
	}
	return 0;
}