LCT 模板及套路總結


這一個月貌似已經考了無數次\(LCT\)了.....
保險起見還是來一發總結吧.....

A. LCT 模板

\(LCT\) 是由大名鼎鼎的 \(Tarjan\) 老爺發明的。
主要是用來維護樹上路徑問題的。 它的神奇之處在於可以直接把一條路徑摳出來維護。
其實就是維護樹鏈剖分中的重鏈與輕鏈。
網上相關教程很多,直接給板子(其實是我懶得打E_E)

0. Splay代碼:

\(LCT\)是以\(Splay\) 為實現基礎的:

IL bool Son(RG int x){return ch[fa[x]][1] == x;}
IL bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }

IL void Rot(RG int x){
	RG int y = fa[x] , z = fa[y] , c = Son(x);
	if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
	ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
	ch[x][!c] = y; fa[y] = x; PushUp(y);
}
IL void Splay(RG int x){
	RG int top = 0; stk[++top] = x;
	for(RG int i = x; !Isroot(i); i = fa[i])stk[++top] = fa[i];
	while(top)PushDown(stk[top--]);
	for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
		if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
	PushUp(x); return;
}

1. Access(x) 操作

作用是把 \(x\) 到根節點的這條路徑變為重鏈。

void Access(int x){
     for(RG int y=0;x;y=x,x=fa[x])
         Splay(x) , ch[x][1] = y , PushUp(x);
     }
}

2. MakeRoot(x) 操作

作用是使 \(x\) 節點成為原樹中的根。

IL void Makeroot(RG int x){Access(x); Splay(x); Reverse(x);}

對應的輔助函數:

IL bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
IL void PushDown(RG int x){
    if(rev[x])
        rev[x] ^= 1 ,
        Reverse(ch[x][0]) , Reverse(ch[x][1]) ;
}

3. FindRoot(x) 操作

作用是找到\(x\)所在原樹的根節點。

IL int FindRoot(RG int x){
    Access(x); Splay(x); 
    while(ch[x][0])x=ch[x][0]; return x;
}

4. Split(x,y) 操作

作用是分離出\(x\)\(y\)這條路徑。

IL void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}

那么此時路徑信息都存在節點 \(y\) 上了,要查詢什么直接查即可。

5. Link(x,y) 操作

作用是連接兩個結點\(x\) , \(y\)

IL void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y; }

6. Cut(x,y) 操作

作用是刪除\(x\) , \(y\) 之間的連邊

IL void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}

7. PushUp(x) \ PushDown(x) 操作

其實是與線段樹一樣的,直接維護題目所求即可。
代碼略,以實際題目為准(注意翻轉左右兒子的\(PushDown\)是必須有的)。

8.貼一發完整的\(LCT\)模版


namespace Link_Cut_Tree{
	
	bool Son(RG int x){return ch[fa[x]][1] == x;}
	bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }
	bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}

	void PushUp(RG int x){ ....... }
	void PushDown(RG int x)
        {if(rev[x])Reverse(ch[x][0]) , Reverse(ch[x][1]) , rev[x] ^= 1;}

	void Rot(RG int x){
		RG int y = fa[x] , z = fa[y] , c = Son(x);
		if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
		ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
		ch[x][!c] = y; fa[y] = x; PushUp(y);
	}
	void Splay(RG int x){
		RG int top = 0; stk[++top] = x;
		for(RG int i = x; !Isroot(i) ; i = fa[i])stk[++top] = fa[i];
		while(top)PushDown(stk[top--]);
		for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
			if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
		PushUp(x); return;
	}
	
	void Access(RG int x)
	    { for(RG int y = 0; x; y = x,x = fa[x])Splay(x),ch[x][1] = y,PushUp(x); }
    void MakeRoot(RG int x){Access(x); Splay(x); Reverse(x);}
    int FindRoot(RG int x){Access(x); Splay(x); while(ch[x][0])x=ch[x][0]; return x;}
    void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}
    void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y;  }
    void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}

}

B. LCT 維護 邊權 信息

具體的實現方法有幾種,這里只講一種最簡單、最常用的方法。
對於每一條邊,新建一個點,然后連向對應的兩個原樹節點。
那么常見的維護方法是 代表點的節點不賦值,只有代表邊的節點才賦值。
具體可以見這一道非常經典的題目: [NOI2014]魔法森林
偽代碼為:
\(Link\)操作直接進行即可。

IL void Link(Node u,Node v)
   NewNode(val_edge) ---(give)---> code_edge
   Link(u,code_edge);  Link(v,code_edge);

\(Cut\)操作則需要找到這條邊對應的兩個節點,然后直接刪除即可。

IL void Cut(Edge e)
    Edge e ---(find)--> Node u1,u2 ;  
    Delete(u1,code_e);  Delete(u2,code_e);

這里注意 :
一定要找到對應的節點,而不是code_e的左右兒子!! ,不然保准你 WA 的懷疑人生。
然后一個非常經典的應用 就是 動態求圖的割邊(橋):
具體見這題:[AHOI2005]LANE 航線規划 ;;;;;;;;

C. LCT 維護 子樹 信息

理論上來說\(LCT\)是不適合維護子樹信息的,但總有一些毒瘤的出題人**..
給一篇寫的非常詳細的博客:http://blog.csdn.net/neither_nor/article/details/52979425
怎么搞呢?
先明確一下概念。
\(LCT\) 鏈剖后,一些路徑變為了 單獨的 一棵\(Splay\)
我們類似樹鏈剖分,把節點划為實兒子、虛兒子。

實兒子就是在同一棵\(Splay\)里的那個兒子,其它則為虛兒子。
那么實兒子對應的子樹就為實子樹,虛兒子對應的子樹就為虛子樹。
.
其實我們原來的\(LCT\)就是維護了實子樹對吧?
所以我們在維護子樹信息時,只要再維護一下虛子樹的信息即可。
以求子樹大小為例。
我們假設\(sz[u]\)表示\(u\)虛子樹大小 , \(sum[u]\)表示整個子樹大小,
那么顯然,我們\(PushUp\)時,只需要更新\(sum\) , \(sz\)我們手動維護。

IL void PushUp(RG int x){ sum[x] = sum[ch[x][0]]+sum[ch[x][1]]+sz[x]+1; }

那么考慮什么情況下會改變虛子樹的大小,其實只有兩個:

1. Access'(x) 操作

我們會把 \(x\) 原來的實兒子變為虛兒子,把另一個虛兒子變為實兒子。
我們對應的修改一下\(x\)\(sz\)值,然后\(PushUp\)修改\(sum\)值即可。

IL void Access(RG int x){
	for(RG int y = 0; x; y = x,x = fa[x]){
		Splay(x);
		sz[x] += sum[ch[x][1]] - sum[y];
		ch[x][1] = y; PushUp(x); 
	}return;
}

2. Link'(x,y) 操作

我們把\(x\)變為\(y\)的兒子。
那么顯然\(x\)以及其所有祖先的\(sz\)值都會收到影響。
解決辦法非常簡單,把\(y\)\(MakeRoot\) 一下即可使得\(x\)只影響 \(y\)
注意一下由於我們修改了\(y\)\(sz\)值,所以要\(PushUp\)一下\(y\)節點。

IL void Link(RG int x,RG int y){
        if((!x)||(!y))return; 
	MakeRoot(x); MakeRoot(y);
	fa[x] = y; sz[y] += sum[x]; PushUp(y); 
}

3.應用

應用一般就是求解會不斷換根的子樹信息問題。
最經典的一道題目是:「BJOI2014」大融合

D.LCT 維護圖上信息

理論上來說\(LCT\)也是不適合維護圖上信息的,但總有一些毒瘤的出題人..
圖上信息的維護是不支持刪邊操作的。
其實圖與樹的差別就是可能有重邊、環之類的。
那么既然我們只需要支持加邊操作,那么只要用並查集維護縮點即可。
怎么維護呢? 這其實也是維護雙聯通分量的套路了。
.
開兩個並查集:
(1)\(bzj[u][1]\),用於維護連通性
每次\(Link\)前,先查找一下兩個點是否聯通,如果不連通,則直接相連即可。
如果聯通,則需要用到第二個並查集:
(2)\(bzj[u][2]\),用於維護雙聯通性
如果兩個點已經雙聯通了,那么再加邊其實對雙聯通性沒有影響。
如果兩個點的\(bzj2\)未相連,那么把兩個的\(bzj2\)相連,然后縮點。
如果兩個點的\(bzj2\)已相連,那么說明這兩個點已經縮在一起了,所以無需連邊。
.
那么此時\(bzj2\)其實就是每個點對應的縮點后的點\(id\)了。
然后是實現,實現的時候,想一想\(LCT\)的性質,我們可以發現:
只有向上跳父親的時候,縮點才會對操作有影響
為了方便描述,定義一個\(F\)函數用於找到一個點\(u\)的對應\(id\)

int Find(RG int x,RG int od){return(bzj[x][od]==x)?x:Find(bzj[x][od],od);}
int F(RG int x){return Find(x,2); }

然后正常的\(LCT\)操作都遵循上面那個結論,找父親是\(Find\)一下即可。
\(Splay\)為例(什么\(Access\)之類的都是一樣的):

IL void Splay(RG int x){
    RG int top = 0; stk[++top] = x;
    for(RG int i = x; !Isroot(i); i = F(fa[i]))stk[++top] = F(fa[i]); 
    while(top)PushDown(stk[top--]);
    for(RG int y = F(fa[x]); !Isroot(x); Rot(x),y = F(fa[x]))
        if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
    PushUp(x);  return;
}

關鍵:\(Add(x,y)\) 操作

關鍵在於鏈接兩個點時的操作,這時候是不能直接執行\(Link\)操作的。
先給代碼:

IL void Add(RG int u,RG int v){
    RG int x = F(u) , y = F(v);
    RG int f1 = Find(x,1) , f2 = Find(y,1);
    if(f1 != f2){
        bzj[f1][1] = f2;
        Link(x,y);  return;
    }
    else{
        if(x == y)return;
        Split(x,y); Merge(y,y);   //縮點
        ch[y][0] = ch[y][1] = 0;  //!!!!!!!!
    }return;
}

顯然就是按照上面的並查集維護原則連邊。
然后給一下縮點的代碼:

IL void Merge(RG int u,RG int rt){
    if(u ^ rt)
        sum[rt] += sum[u], bzj[F(u)][2] = rt, sum[u] = fig[u] = 0;
    if(ch[u][0])Merge(ch[u][0],rt);
    if(ch[u][1])Merge(ch[u][1],rt);
}

其中\(sum\)為結點自身的信息(本身值),\(fig\)為子樹信息(計算值)。

應用:

這個就依題目而定吧。
請記住一個最明顯的特征:只支持加邊,不支持刪邊!
\(LCT\)維護圖上信息最經典的一題為:[BZOJ 2959]長跑 , 友情提示注意卡常。

有根LCT

自行yy,唯一一個需要注意的地方:
Cut(u,Fa) 的時候,必須把\(fa\)轉到\(Splay\)頂端!(不能把\(u\)轉到頂端),然后剪切。

IL void Cut(int u , int Fa) {
    Access(u) ; Splay(Fa) ;
    ch[1][Fa] = fa[u] = 0 ; PushUp(Fa) ; return ; 
}

因為如果轉\(u\)到頂端的話,剪切后整棵樹的根就會產生變化,轉移到\(u\)所屬\(Splay\)中。


免責聲明!

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



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