Link Cut Tree學習筆記

liuchanglc 發表於 2021-01-18

定義

動態樹問題 ,是一類要求維護一個有根樹森林,支援對樹的分割, 合併等操作的問題。

\(RobertE.Tarjan\) 為首的科學家們提出解決演算法 \(Link-Cut Trees\),簡稱 \(lct\)

在處理樹上的很多詢問問題的時候,我們常常會用到輕重鏈剖分,但是它只能維護當樹的形態保持不變的時候的資訊。

那麼在樹形態發生變化的時候,輕重鏈剖分就失去了效果,這個時候我們就要用到 \(lct\)

個人感覺這篇部落格講的不錯

前置知識:spaly

\(splay\)\(lct\) 的輔助樹

如果不會 \(splay\) 可以去我的部落格學習一下

性質

\(1\)、和輕重鏈剖分相同的是,\(lct\) 也對要維護的樹進行了劃分,將鏈分為了實鏈和虛鏈。

每一個 \(Splay\) 維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷 \(Splay\) 得到的每個點的深度序列嚴格遞增。

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

\(3\)、邊分為實邊和虛邊,實邊包含在 \(Splay\) 中,而虛邊總是由一棵 \(Splay\) 指向另一個節點(指向該 \(Splay\) 中中序遍歷最靠前的點在原樹中的父親)。

如果一個節點有多個兒子,那麼該節點只會認用實邊相連的那個兒子,對於虛邊相連的兒子,則會認父不認子。

操作

1、結構體定義

個人的寫法是把 \(lct\) 封裝到了結構體裡

int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
//ch[0/1]:左/右兒子  fa:父親節點 val:該節點的權值 
//sum:該節點及其子樹的權值之和 sta&top:push_down時用 rev:翻轉標記

2、標記上傳和下放

類似於線段樹

要注意的是,線段樹的父親節點並不是一個真實的節點,而 \(spaly\) 的父親節點代表了原樹中真實存在的一個節點

void push_up(rg int da){
	sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
}
void push_down(rg int da){
	rg int lc=ch[da][0],rc=ch[da][1];
	if(rev[da]){
		rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
		std::swap(ch[da][0],ch[da][1]);
	}
}

3、isroot操作

判斷某個節點是否是該節點所在的 \(splay\) 的根節點

利用了性質 \(3\)

bool isroot(rg int da){
	return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
}

4、splay操作

\(splay\) 板子的區別就是多了 \(push\_down\) 操作

void xuanzh(rg int x){
	rg int y=fa[x];
	rg int z=fa[y];
	rg int k=(ch[y][1]==x);
	if(!isroot(y)){
		ch[z][ch[z][1]==y]=x;
	}
	fa[x]=z;
	ch[y][k]=ch[x][k^1];
	fa[ch[x][k^1]]=y;
	ch[x][k^1]=y;
	fa[y]=x;
	push_up(y);
	push_up(x);
}
void splay(rg int x){
	sta[top=1]=x;
	for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
	for(rg int i=top;i>=1;i--) push_down(sta[i]);
    //把需要下傳標記的提前存起來一起修改
	while(!isroot(x)){
		rg int y=fa[x];
		rg int z=fa[y];
		if(!isroot(y)){
			(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
		}
		xuanzh(x);
	}
}

5、access操作

比較重要的一個操作

目的是打通根節點到指定節點的實鏈,使得一條中序遍歷以根開始、以指定點結束的 \(Splay\) 出現

void access(rg int x){
	for(rg int y=0;x;y=x,x=fa[x]){
		splay(x);
		ch[x][1]=y;
		push_up(x);
	}
}

6、makeroot操作

使某一個節點成為整個聯通塊的根

\(access\) 之後已經打通了一條當前點到根節點的路徑

此時 \(x\)\(Splay\) 中一定是深度最大的點

\(x\ splay\) 一下,\(x\) 將沒有右子樹(性質\(1\)

於是翻轉整個 \(Splay\),使得所有點的深度都倒過來了

\(x\) 沒了左子樹,成了深度最小的點(根節點),達到了我們的目的

要注意的是,換根操作之後,原樹的形態就改變了

void makeroot(rg int x){
	access(x);
	splay(x);
	rev[x]^=1;
	push_down(x);
}

7、findroot操作

找出指定點所在的聯通塊的根節點

和上一個操作一樣,只不過這一次我們不再進行翻轉操作,而是一直跳左子樹

最後跳到的節點就是根

int findroot(rg int x){
	access(x);
	splay(x);
	while(ch[x][0]){
		push_down(x);
		x=ch[x][0];
	}
	splay(x);
	return x;
}

8、split操作

提取 \(x\)\(y\) 的路徑

先讓 \(x\) 成為根節點,再打通 \(y\) 到根節點的路徑

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

9、刪邊、連邊操作

先讓 \(x\) 成為根節點,再判斷聯通性

void link(rg int x,rg int y){
	makeroot(x);
	if(findroot(y)!=x) fa[x]=y;
}
void cut(rg int x,rg int y){
	makeroot(x);
	if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
		fa[y]=ch[x][1]=0;
		push_up(x);
	}
}

10、求LCA操作

兩遍 \(access\)

int access(rg int x){
   for(rg int y=0;x;y=x,x=fa[x]){
   	splay(x);
   	ch[x][1]=y;
   	push_up(x);
   }
   return y;// 多了一個返回值
}

然後求 \(LCA\) 的過程就出來了。

int LCA(int x,int y){ 
   if(findroot(x)!=findroot(y))// 必須的特判。
   	return -1;
   access(x);
   return access(y); 
} 

完整模板

洛谷P3690

#include<cstdio>
#include<iostream>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
int n,m;
struct LCT{
	int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
	void push_up(rg int da){
		sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
	}
	void push_down(rg int da){
		rg int lc=ch[da][0],rc=ch[da][1];
		if(rev[da]){
			rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
			std::swap(ch[da][0],ch[da][1]);
		}
	}
	bool isroot(rg int da){
		return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
	}
	void xuanzh(rg int x){
		rg int y=fa[x];
		rg int z=fa[y];
		rg int k=(ch[y][1]==x);
		if(!isroot(y)){
			ch[z][ch[z][1]==y]=x;
		}
		fa[x]=z;
		ch[y][k]=ch[x][k^1];
		fa[ch[x][k^1]]=y;
		ch[x][k^1]=y;
		fa[y]=x;
		push_up(y);
		push_up(x);
	}
	void splay(rg int x){
		sta[top=1]=x;
		for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
		for(rg int i=top;i>=1;i--) push_down(sta[i]);
		while(!isroot(x)){
			rg int y=fa[x];
			rg int z=fa[y];
			if(!isroot(y)){
				(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
			}
			xuanzh(x);
		}
	}
	void access(rg int x){
		for(rg int y=0;x;y=x,x=fa[x]){
			splay(x);
			ch[x][1]=y;
			push_up(x);
		}
	}
	void makeroot(rg int x){
		access(x);
		splay(x);
		rev[x]^=1;
		push_down(x);
	}
	int findroot(rg int x){
		access(x);
		splay(x);
		while(ch[x][0]){
			push_down(x);
			x=ch[x][0];
		}
		splay(x);
		return x;
	}
	void split(rg int x,rg int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)!=x) fa[x]=y;
	}
	void cut(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
			fa[y]=ch[x][1]=0;
			push_up(x);
		}
	}
}lct;
int main(){
	n=read(),m=read();
	for(rg int i=1;i<=n;i++) lct.sum[i]=lct.val[i]=read();
	rg int aa,bb,cc;
	for(rg int i=1;i<=m;i++){
		aa=read(),bb=read(),cc=read();
		if(aa==0){
			lct.split(bb,cc);
			printf("%d\n",lct.sum[cc]);
		} else if(aa==1){
			lct.link(bb,cc);
		} else if(aa==2){
			lct.cut(bb,cc);
		} else {
			lct.splay(bb);
			lct.val[bb]=cc;
		}
	}
	return 0;
}

複雜度證明

傳送門

一些題目

型別一、維護鏈資訊

[國家集訓隊]Tree II

注意區間標記先乘後除

然後就和普通的線段樹一樣標記下放即可

如果沒有刪邊和加邊操作,也可以用樹鏈剖分實現,但是複雜度要多一個 \(log\)

有的時候要維護的資訊不是在點上而是在邊上

此時我們就需要把邊看成點

例如有一條邊 \((u,v)\),編號為 \(id\)

那麼我們需要在 \(lct\) 中連兩條邊 \((u,id+n)(v,id+n)\)

邊的資訊儲存在 \(id+n\) 這個點上

型別二、動態維護聯通性

[SDOI2008]洞穴勘測

用可撤銷並查集按秩合併也可以做

換成 \(lct\) 也是一個板子

只要判斷兩個點 \(findroot\) 得到的值是否一樣即可

長跑

由於圖中會出現邊雙,不能重複計算,所以需要將邊雙縮點

用並查集維護當前的點屬於哪一個雙聯通分量

考慮合併 \(x\)\(y\) ,如果 \(x\)\(y\)\(lct\) 上不是聯通的,那麼直接連邊

否則我們先取出 \(x\)\(y\) 的路徑,將路徑上所有權值都加到 \(y\) 上,同時把路徑上所有點的父親設為 \(y\)

要注意的是每次操作之前都要在並查集裡找一下祖先

型別三、最小生成樹一類的問題

最小差值生成樹

[NOI2014]魔法森林

[Cnoi2019]須臾幻境

[WC2006]水管局長

這種題大部分都需要在邊權上維護一個最大/最小值,然後按照邊權從大到小/從小到大排序

遍歷到一條邊時,如果這條邊所連的兩個點已經在同一個同一個聯通塊中,根據需要刪邊/加邊

型別四、維護子樹資訊

P4219 [BJOI2014]大融合

因為 \(LCT\) 中的兒子有實兒子和虛兒子之分

\(splay\) 中儲存的只是實兒子的資訊

所以我們要新開一個變數 \(siz2\) 記錄虛子樹的大小

相應地一些函式也要被修改

struct LCT{
	int ch[maxn][2],siz2[maxn],siz[maxn],top,sta[maxn],rev[maxn],wz[maxn],fa[maxn];
	void push_up(rg int da){
		siz[da]=siz[ch[da][1]]+siz[ch[da][0]]+1+siz2[da];//1
	}
	void push_down(rg int da){
		rg int lc=ch[da][0],rc=ch[da][1];
		if(rev[da]){
			rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
			std::swap(ch[da][0],ch[da][1]);
		}
	}
	bool isroot(rg int da){
		return ch[fa[da]][0]!=da&&ch[fa[da]][1]!=da;
	}
	void xuanzh(rg int x){
		rg int y=fa[x];
		rg int z=fa[y];
		rg int k=(ch[y][1]==x);
		if(!isroot(y)){
			ch[z][ch[z][1]==y]=x;
		}
		fa[x]=z;
		ch[y][k]=ch[x][k^1];
		fa[ch[x][k^1]]=y;
		ch[x][k^1]=y;
		fa[y]=x;
		push_up(y);
		push_up(x);
	}
	void splay(rg int x){
		top=1;
		sta[top]=x;
		for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
		for(rg int i=top;i>=1;i--) push_down(sta[i]);
		while(!isroot(x)){
			rg int y=fa[x];
			rg int z=fa[y];
			if(!isroot(y)){
				(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
			}
			xuanzh(x);
		}
	}
	void access(rg int x){
		for(rg int y=0;x;y=x,x=fa[x]){
			splay(x);
			siz2[x]+=siz[ch[x][1]]-siz[y];//2
			ch[x][1]=y;
			push_up(x);
		}
	}
	void makeroot(rg int x){
		access(x);
		splay(x);
		rev[x]^=1;
		push_down(x);
	}
	void split(rg int x,rg int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(rg int x,rg int y){
		makeroot(x);
		makeroot(y);
		fa[x]=y;
		siz2[y]+=siz[x];//3
	}
	void cut(rg int x,rg int y){
		split(x,y);
		fa[x]=ch[y][0]=0;
		push_up(y);
		makeroot(x);
		makeroot(y);
	}
}lct;

型別五、查詢k級祖先

loj#6710. 「RCOI2019」維護

若設每個點的點權均為 \(1\),刪除時把點權置為 \(0\)

則這樣得到的 \(k\) 級祖先就是到這個點的路徑上權值和為 \(k\) 的祖先

所以在 \(lct\) 上二分查詢即可

程式碼

int find(rg int now,rg int ke){
	access(now);
	splay(now);
	if(!now || sum[ch[now][0]]<ke) return 0;
	if(ke==0) return now;
	now=ch[now][0];
	while(now){
		if(sum[ch[now][1]]>=ke) now=ch[now][1];
		else if(sum[ch[now][1]]+val[now]==ke) return splay(now),now;
		else {
			ke-=(sum[ch[now][1]]+val[now]);
			now=ch[now][0];
		}
	}
	return 233;
}

相關文章