實鏈剖分和樹鏈剖分的區別
樹鏈剖分有一個更專業的名稱 :輕重鏈剖分,即為根據子節點的子樹大小來剖,雖然樹鏈剖分有很好的性質 ,但是還是存在缺陷的。例如 : 樹鏈剖分將樹剖完之后是靜態的,(無法進行修改了,但不代表就不能換根了。)也就是說樹鏈剖分只能針對於樹的結構不變的情況下操作。
實鏈剖分 : 將樹的邊分為兩種,一種是實邊,一種是虛邊,維護的時候則是對實邊進行維護。
我們發現實鏈剖分很不固定,將樹的邊划分實虛的話,也無法保證形態。如果我們用一個靈活的數據結構,那么我們發現,其實這個樹完全可以動起來,因為任意轉化實虛邊都可以維護,也就是說,刪除一條邊,加上一條邊,對於實鏈剖分來說,都可以。
然后我們將實鏈剖分剖出來的鏈用 \(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 ;
}
Link 操作
在 \(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) ;
}
P3690 【模板】動態樹(Link Cut Tree)
囊括了幾乎上文所有內容 。
因為上文都說了,所以這里也是直接給出代碼了。不過這個是早寫的,所以用的是結構體存的,不過沒什么兩樣
//
/*
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…\)