淺談 LCT


實鏈剖分和樹鏈剖分的區別

樹鏈剖分有一個更專業的名稱 :輕重鏈剖分,即為根據子節點的子樹大小來剖,雖然樹鏈剖分有很好的性質 ,但是還是存在缺陷的。例如 : 樹鏈剖分將樹剖完之后是靜態的,(無法進行修改了,但不代表就不能換根了。)也就是說樹鏈剖分只能針對於樹的結構不變的情況下操作。

實鏈剖分 : 將樹的邊分為兩種,一種是實邊,一種是虛邊,維護的時候則是對實邊進行維護。

我們發現實鏈剖分很不固定,將樹的邊划分實虛的話,也無法保證形態。如果我們用一個靈活的數據結構,那么我們發現,其實這個樹完全可以動起來,因為任意轉化實虛邊都可以維護,也就是說,刪除一條邊,加上一條邊,對於實鏈剖分來說,都可以。

然后我們將實鏈剖分剖出來的鏈用 \(Splay\) 維護,這種數據結構叫做 \(LCT\)\(Link-Cut-Tree\)

不知道為什么 \(LCT\) 的一些博客講解中以 \(Splay\) 去維護輕重鏈剖分的鏈。


LCT 的一些淺顯的概念理解

大概有輔助樹, \(Splay\) 與輔助樹的關系之類。

輔助樹

可以簡單的理解為一些 \(Splay\) 構成了輔助樹。我們給出一張圖來理解一下其結構 :

通過對比第一個圖和第二個圖,我們可以知道原樹中的實鏈對應着輔助樹的實鏈。無論怎么變換都是一條條的實鏈都是不會變得。

同時,因為我們選擇用 \(Splay\) 維護一條實鏈,那么我們也就可以認為左邊綠框框也就是一個 \(Splay\) , 然后我們顯然可以知道這些 \(Splay\) 是通過虛邊連接起來的(也就是紅邊連接起來的)。

然后我們考慮是怎么構造的這一顆輔助樹 :
首先我們通過實鏈構造出一顆顆的 \(Splay\) , 即為 :

\(\{A - D - C \} ,\{ E - C \} , \{ F \}\) 總共三個 \(Splay\)

然后我們令 \(E , F\) 去尋找他在原樹中的父親,也就是 \(A , C\) , 然后通過虛邊連接起來。
這里有一個不成文的規定 : 認父不認子

最后就構造完了。


輔助樹和原樹的區別

  • 輔助樹的根不一定是原樹的根。
  • 原樹父親的指向不等同於輔助樹父親的指向
  • 輔助樹是可以在 \(Splay\) 的幫助下,實現任意換根。
  • 輔助樹中不存在節點指向子節點的情況。(但可以有節點統計子節點的情況)

LCT 的一些性質

  • 每一個 \(Splay\) 維護的是一條在原樹中深度嚴格遞增的樹鏈,且中序遍歷 \(Splay\) 得到的每一個點的深度組成的序列也是嚴格遞增的。

  • 每一個節點包含且僅包含於一個 \(Splay\)

  • 認父不認子

邊分為實邊和虛邊,實邊包含在 \(Splay\) 中,而虛邊總是由一棵 \(Splay\) 指向另一個節點(指向該 \(Splay\) 中中序遍歷最靠前的點在原樹中的父親)。
因為性質 \(2\),當某點在原樹中有多個兒子時,只能向其中一個兒子拉一條實鏈(只認一個兒子),而其它兒子是不能在這個 \(Splay\) 中的。
那么為了保持樹的形狀,我們要讓到其它兒子的邊變為虛邊,由對應兒子所屬的 \(Splay\) 的根節點的父親指向該點,而從該點並不能直接訪問該兒子(認父不認子)。


LCT 的一些操作


Access(x) 操作

因為性質 \(3\) ,建立了虛邊,而我們選擇維護的卻是實鏈,所以會導致根節點 (以下均稱為 \(rt\) ) 到 \(x\) 的路徑經過所有的邊不一定全都是實邊,即 \(rt\)\(x\) 的路徑不通。
\(Access(x)\) 的意思為 將 \(rt\)\(x\) 的路徑打通,也就是將 \(rt \to x\) 的路徑上所有的經過的邊都轉化為實邊。

這是 \(LCT\) 最核心的部分 (就屬 \(Splay\) 的代碼最長)

這里以 \(FlashHu\) 大佬的博文 LCT總結——概念篇 中的例子予以說明。 他講的特別詳細,我不認為我能比他講的還要詳細。


有一棵樹,假設一開始實邊和虛邊是這樣划分的(虛線為虛邊)

那么所構成的 \(LCT\) 可能會長這樣(綠框中為一個 \(Splay\),可能不會長這樣,但只要滿足中序遍歷按深度遞增(性質 \(1\))就對結果無影響)

現在我們要 \(Access(N)\),把 \(A−N\) 的路徑拉起來變成一條 \(Splay\)
因為性質 \(2\) ,該路徑上其它鏈都要給這條鏈讓路,也就是把每個點到該路徑以外的實邊變虛。
所以我們希望虛實邊重新划分成這樣。

然后怎么實現呢?
我們要一步步往上拉。
首先把 \(splay(N)\),使之成為當前 \(Splay\) 中的根。
為了滿足性質 \(2\),原來 \(N−O\) 的重邊要變輕。
因為按深度O在N的下面,在 \(Splay\) 中O在 \(N\) 的右子樹中,所以直接單方面將 \(N\) 的右兒子置為 \(0\)(認父不認子)
然后就變成了這樣——

我們接着把 \(N\) 所屬 \(Splay\) 的虛邊指向的 \(I\)(在原樹上是 \(L\) 的父親)也轉到它所屬 \(Splay\) 的根,\(splay(I)\)
原來在 \(I\) 下方的重邊 \(I−K\) 要變輕(同樣是將右兒子去掉)。
這時候 \(I−L\) 就可以變重了。因為 \(L\) 肯定是在 \(I\) 下方的(剛才 \(L\) 所屬 \(Splay\) 指向了\(I\)),所以I的右兒子置為 \(N\),滿足性質 \(1\)
然后就變成了這樣——

\(I\) 指向 \(H\),接着 \(splay(H)\)\(H\) 的右兒子置為 \(I\)

\(H\) 指向 \(A\),接着 \(splay(A)\)\(A\) 的右兒子置為 \(H\)

\(A−N\) 的路徑已經在一個 \(Splay\) 中了,大功告成!
代碼其實很簡單。。。。。。循環處理,只有四步——

歸根到底,其實就是 :
\(u\) 的右兒子為 \(v\) 的時候,我們就認為 \(u - v\) 是一條實邊。
顯然 \(Splay\) 是維護實鏈的,如果我們 \(1 \to n\) 是連通的,那么我們直接查詢舉行了。

同樣的。如果不連通,那么就以為着我們需要將這一條鏈賦值成實鏈。我們按照上面圖的模擬過程來即可。

模擬過程可以簡化為 :

  • 旋轉到當前 \(Splay\) 的根。
  • 建立和父親的實邊關系。
  • 更新節點維護的信息。

\(Question\) : 我們需不需要考慮當前和 \(u\) 這個點連接的實鏈,把他置換成虛邊呢?
\(Answer\) : 不需要,這個時候就體現出我們認父不認子的好處了,我們直接將 \(u\) 這個點的右兒子替換掉,就代表 \(u\) 這個點的右兒子已經處理完了。

qaq void Access(int u) {
	for(qwq int y = 0 ; u ; u = f[y = u]) 
	 Splay(u) , ch[u][1] = y , pushup(u) ; 
	// 先旋轉到當前 Splay 的根,然后通過 f[u] 建立的虛邊找到父親節點,同時將父親節點
	// 的右兒子賦為當前的這個點,形成實邊,同時連接該節點和父親所在的 Splay  。
	// pushup 即為更新維護的信息 
}

MakeRoot(x) 操作

就像他的意譯一樣, \(MakeRoot\) ,使成為根。缺賓語

那么如何操作呢 。我們上文已經知道了如何將打通一個點到根的路徑了。

這時候用到 \(Access(x)\)\(Splay(x)\) 操作了。

我們這個 \(Splay\) 滿足性質 \(1\), 所以 \(Access(x)\) 之后 , \(x\) 還是深度最大的點。

我們將其 \(Splay\) 旋轉一下,本來它就是最大的,顯然 \(x\) 在這個 \(Splay\) 中沒有右子樹。

於是我們翻轉整個 \(Splay\) , 使得所有點的深度都倒過來,\(x\) 沒有了左子樹,它成了深度最小的點,那 \(x\) 其實不就是樹根了嘛。

qaq void MakeRoot(int u) {
	Access(u) , Splay(u) , PushOver(u) ; 
	// PushOver(u) 就是翻轉操作
}

FindRoot 操作

找樹根 。
\(Access(x)\) 之后 \(x\) 不就是深度最大的點了嘛,我們就不斷去找左子樹左子樹,也就是去尋找深度最小的點,當節點 \(u\) 沒有左子樹的時候,他的深度也就是最小的了,那么 \(u\) 就是樹根了。

當然,其中有可能會有 \(tag\) 標記,也就是區間翻轉標記,我們這里直接下傳即可,不下傳無法保證 \(u\) 一定是樹根。 解釋的話,分析上一個操作。

qaq void FindRoot(int u) {
	Access(u) ; 
	while(ch[u][0]) pushdown(u) , u = ch[u][0] ; 
	return u ; 
} 

\(LCT\) 中加入一條 \(u - v\) 的邊。

\(u\) 成為樹根 , 然后建立虛邊。

這個地方需要特判一下,因為樹上顯然不能出現環,所以 \(FindRoot(v) \neq u\) ,這樣才讓 \(u\)\(v\) 認父。如果不知為什么 \(u\)\(v\) 認父,則建議重新審視一下 \(Access(x)\) 的模擬過程。

qaq void Link(int u , int v) {
	MakeRoot(u) ; 
	if(FindRoot(v) != u) f[u] = v ; 
}

Split 操作

\(Split(u,v)\)代表是抽出 \(u - v\) 這條路徑成為實鏈。

這時候我們有 \(Link\) 的啟發,我們就可以直接讓 \(u\) 成為樹根。然后通過 \(Access(v)\) 打通\(u - v\) 的路徑即可。

qaq void Split(int u , int v) {
	MakeRoot(u) , Access(v) , Splay(v) ;
}

Cut 操作

刪除 \(u , v\) 這一條邊。

如果題目保證斷邊合法,倒是很方便。

使 \(u\) 為根后 , \(v\) 的父親一定會指向 \(u\) , 且深度相差 \(1\) , 當 \(Access(v) , Splay(v)\) 之后,因為 \(u\) 深度小,所以 \(u\) 一定是 \(v\) 的左兒子。直接斷開連接。

qaq void CUT(int u , int v) {
	Split(u , v) ; f[u] = ch[v][0] = 0 ; pushup(v) ;
}

如果題目不保證斷邊合法,也就是不一定會存在該邊。

那么我們也按照上面一樣,去特判一下。首先使得 \(u\) 成為 \(Splay\) 的根,然后去判斷一下 \(u , v\) 是否在一個子樹內,如果不在,則不存在。接着去判斷一下 \(v\) 的父親是否是 \(u\) ,如果不是,不存在,最后去判斷一下 \(v\) 是否有左兒子,如果沒有,也不行。

qaq void Cut(int u , int v) {
	MakeRoot(u) ; 
	if(FindRoot(v) == u && f[v] = u && !ch[v][0]) 
	f[v] = ch[u][1] = 0 ,pushup(u); 
} 

Splay , Rorate,pushdown,其他操作

和普通平衡樹很相似,但是有幾處是不同的。

這里就直接給出代碼了

qaq bool check(int x) {//判斷節點是否為一個Splay的根(與普通Splay的區別1)
	return ch[f[x]][1] == x || ch[f[x]][0] == x ;
}//原理很簡單,如果連的是輕邊,他的父親的兒子里沒有它
qaq bool jd(int x) {
	return ch[f[x]][1] == x ; 
}
qaq void PushOver(int u) {
	swap(ch[u][1] , ch[u][0]) ;
	tag[u] ^= 1 ; 
}
qaq void pushdown(int u) {
	if(tag[u]) 
	{
		tag[u] = 0 ; 
		if(ch[u][0]) PushOver(ch[u][0]) ; 
		if(ch[u][1]) PushOver(ch[u][1]) ;
	}
}
qaq void Rorate(int x) {
	int y = f[x] , z = f[y] , k = ch[y][1] == x , w = ch[x][k ^ 1] ; 
	if(check(y)) ch[z][ch[z][1] == y] = x ; ch[x][k ^ 1] = y ; ch[y][k] = w ; 
	//額外注意if(check(y))語句,此處不判斷會引起致命錯誤(與普通Splay的區別2)
	if(w) f[w] = y ;f[y] = x ; f[x] = z ; pushup(y) ; 
}
qaq void Splay(int x) {//只傳了一個參數,因為所有操作的目標都是該Splay的根(與普通Splay的區別3)
	int y = x, z = 0 ; sta[++z] = y ; //sta為棧,暫存當前點到根的整條路徑,pushdown時一定要從上往下放標記(與普通Splay的區別4)
	while(check(y)) sta[++z] = y = f[y] ; 
	while(z) pushdown(sta[z--]) ; 
	while(check(x)) 
	{
		y = f[x] , z = f[y] ; 
		if(check(y)) Rorate(jd(x) ^ jd(y) ? x : y) ; 
		Rorate(x) ; 
	}
	pushup(x) ; 
}

囊括了幾乎上文所有內容 。

因為上文都說了,所以這里也是直接給出代碼了。不過這個是早寫的,所以用的是結構體存的,不過沒什么兩樣

//
/*
Author : Zmonarch
Knowledge :
*/
#include <bits/stdc++.h>
#define int long long
#define inf 2147483647
#define qwq register
#define qaq inline
using namespace std ;
const int kmaxn = 1e6 + 10 ;
qaq int read() {
	int x = 0 , f = 1 ; char ch = getchar() ;
	while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ;}
	while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ;}
	return x * f ;
}
int n , m ; 
int f[kmaxn] , rt[kmaxn] , sum[kmaxn] , s[kmaxn]; 
struct SPLAY {
	int val , sum ; 
	bool tag ; // 區間翻轉的標記 
	int ch[2] ; 
	SPLAY() {
	 tag = ch[1] = ch[0] = 0 ; 
	}
}st[kmaxn << 1];
qaq bool check(int x) {
	return (st[f[x]].ch[0] == x) || (st[f[x]].ch[1] == x) ; 
}
qaq void pushup(int u) {
	st[u].sum = st[u].val ^ st[st[u].ch[0]].sum ^ st[st[u].ch[1]].sum ;
}
qaq void Pushover(int u) {
	swap(st[u].ch[1] , st[u].ch[0]) ; 
	st[u].tag ^= 1 ; 
}
qaq void pushdown(int u) {
	if(st[u].tag)
	{
		if(st[u].ch[0]) Pushover(st[u].ch[0]) ; 
		if(st[u].ch[1]) Pushover(st[u].ch[1]) ;
		st[u].tag = 0 ;
	} 
}
qaq void Rorate(int x) {
	int y = f[x] , z = f[y] , k = (st[y].ch[1] == x) , w = st[x].ch[!k] ;
	if(check(y)) st[z].ch[st[z].ch[1] == y] = x ;
	st[x].ch[!k] = y ; st[y].ch[k] = w ; 
	if(w) f[w] = y ; f[y] = x ; f[x] = z ; pushup(y) ; 
}
qaq void Splay(int x) {
	int y = x , z = 0 ; rt[++z] = y ; 
	while(check(y)) rt[++z] = y = f[y] ; 
	while(z) pushdown(rt[z--]) ; 
	while(check(x)) 
	{
		y = f[x] ; z = f[y] ; 
		if(check(y)) Rorate((st[y].ch[0] == x) ^ (st[z].ch[0] == y) ? x : y) ;
		Rorate(x) ; 
	} 
	pushup(x) ;
}
qaq void Access(int u) {
	for(qwq int y = 0 ; u ; y = u , u = f[u]) 
	 Splay(u) , st[u].ch[1] = y , pushup(u) ; 
// 通過虛鏈指定父親,將這個父親旋轉到當前父親所在的 Splay 的根上,更新 u 這個點的右兒子。 
}
qaq void MakeRoot(int u) { // 指定 u 為原樹的根 
	Access(u) ; Splay(u) ; Pushover(u) ; 
}
qaq int FindRoot(int u) {
	Access(u) ; Splay(u) ; 
	while(st[u].ch[0]) pushdown(u) , u = st[u].ch[0] ; 
	Splay(u) ; return u ;   
}
qaq void Split(int u , int v) { // 使得 u , v 這一條鏈能在一個 Splay 中 
	MakeRoot(u) ; Access(v) ; Splay(v) ; 
	// 先讓 u 成為根,然后直接 Access 打通 v 到根 
}
qaq void Link(int u , int v) { // 判斷連一條 u , v 的邊是否合法  
	MakeRoot(u) ; 
	if(FindRoot(v) != u)  f[u] = v ; // u -> v 的邊
	// u 已經是 Splay 的了,根據認父不認子,所以直接向這個根連 
}
// 這是保證存在該邊的情況 
qaq void Cut(int u , int v) { // 斷開 u - v 這條邊 
	Split(u , v) ; f[u] = st[v].ch[1] = 0 ; pushup(u) ;  
}
// 這是不保證存在該邊的情況
qaq void Pre_Cut(int u , int v) {
	MakeRoot(u) ; 
	if(FindRoot(v) == u && f[v] == u && !st[v].ch[0]) f[v] = st[u].ch[1] = 0 , pushup(u) ;  
}
signed main() {
	n = read() , m = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) st[i].val = read() ; 
	for(qwq int i = 1 ; i <= m ; i++) 
	{
		int opt = read() , x = read() , y = read() ; 
		if(opt == 0) Split(x , y) , printf("%lld\n" , st[y].sum) ; 
		if(opt == 1) Link(x , y) ;
		if(opt == 2) Pre_Cut(x , y) ; 
		if(opt == 3) Splay(x) , st[x].val = y ; 
	}
	return 0 ;
}

題單

這里就是照搬 \(FlashHu\) 大佬的 LCT總結——應用篇(附題單)(LCT) 這篇博客了。

維護鏈信息(LCT上的平衡樹操作)

P3690 【模板】Link Cut Tree
P3203 [HNOI2010]彈飛綿羊
P1501 [國家集訓隊]Tree II
P2486 [SDOI2011]染色
P4332 [SHOI2014]三叉神經樹


動態維護連通性&雙聯通分量

P2147 [SDOI2008] 洞穴勘測
P3950 部落沖突
P2542 [AHOI2005]航線規划
BZOJ4998 星球聯盟
BZOJ2959 長跑


維護邊權(常用於維護生成樹)

P4172 [WC2006]水管局長
UOJ274溫暖會指引我們前行
P4180 [BJWC2010]嚴格次小生成樹
P4234 最小差值生成樹
P2387 [NOI2014] 魔法森林


維護子樹信息

P4219 [BJOI2014]大融合
U19482 山村游歷(Wander)
#3510. 首都
SP2939 QTREE5 - Query on a tree V
#558. 「Antileaf's Round」我們的 CPU 遭到攻擊


維護樹上染色聯通塊

P2173 [ZJOI2012]網絡
P3703 [SDOI2017]樹點塗色
SP16549 QTREE6 - Query on a tree VI
SP16580 QTREE7 - Query on a tree VII
#3914. Jabby's shadows


特殊題型

#207. 共價大爺游長沙
P3348 [ZJOI2016]大森林
P4338 [ZJOI2018]歷史
#2289. 「THUWC 2017」在美妙的數學王國中暢游


\(ans \ \ so\ \ on…\)


鳴謝

LCT總結——概念篇
OI-Wiki -- Link Cut Tree
LCT總結——應用篇(附題單)(LCT)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM