1. 程式人生 > >LCT(Link-Cut Tree)詳解(蒟蒻自留地)

LCT(Link-Cut Tree)詳解(蒟蒻自留地)

 最近自學了LCT,發現網上的資料講解不是很全面,像我這樣的蒟蒻一時半會根本理解不了。我弄了很久總算是理解了LCT,打算總結一下LCT的基本操作,還請諸位神牛來找找茬。

如果你還沒有接觸過LCT,你可以先看一看這裡:

看完之後我們知道,LCT和靜態的樹鏈剖分很像。怎麼說呢?這兩種樹形結構都是由若干條長度不等的“重鏈”和“輕邊”構成(名字可以不同,大概就是這個意思),“重鏈”之間由”輕邊”連線。就像這樣:


可以想象為一棵樹被人為的砍成了一段段。

        LCT和樹鏈剖分不同的是,樹鏈剖分的鏈是不會變化的,所以可以很方便的用線段樹維護。但是,既然是動態樹,那麼樹的結構形態將會發生改變,所以我們要用更加靈活的維護區間的結構來對鏈進行維護,不難想到Splay可以勝任。如何分離樹鏈也是保證時間效率的關鍵(鏈的數量和長度要平衡),樹鏈剖分的“重兒子”就體現了前人博大精深的智慧。

        在這裡解釋一下為什麼要把樹砍成一條條的鏈:我們可以在logn的時間內維護長度為n的區間(鏈),所以這樣可以極大的提高樹上操作的時間效率。在樹鏈剖分中,我們把一條條鏈放到線段樹上維護。但是LCT中,由於樹的形態變化,所以用能夠支援合併、分離、翻轉等操作的Splay維護LCT的重鏈(注意,單獨一個節點也算是一條重鏈)。

        這時我們注意到,LCT中的輕邊資訊變得無法維護。為什麼呢?因為Splay只維護了重鏈,沒有維護重鏈之間的輕邊;而LCT中甚至連根都可以不停的變化,所以也沒法用點權表示它父邊的邊權(父親在變化)。所以,如果在LCT中要維護邊上資訊,個人認為最方便的方法應該是把邊變成一個新點和兩條邊。這樣可以把邊權的資訊變成點權維護,同時為了不影響,把真正的樹上節點的點權變成0,就可以用維護點的方式維護邊。

LCT的各種操作:

        LCT中用Splay維護鏈,這些Splay叫做“輔助樹“。輔助樹以它上面每個節點的深度為關鍵字維護,就是輔助樹中每個節點左兒子的深度小於當前節點的深度,當前節點的深度小於右兒子的深度。

        可以把LCT認為是一個由Splay組成的森林,就像這樣:(三角形代表一棵Splay,對應著LCT上一條鏈)


箭頭是什麼意思呢?箭頭記錄著某棵Splay對應的鏈向上由輕邊連著哪個節點,可以想象為箭頭指向“Splay 的父親”。但是,Splay的父親並不記錄有這個兒子,即箭頭是單向的。同時,每個節點要記錄它是否是它所在的Splay的根。這樣,Splay構成的森林就建成了。

這個是我的Splay節點最基本的定義:(如果要維護更多資訊就像Splay維護區間那樣加上更多標記)

struct node{
	int fa,ch[2]; //父親和左右兒子。
	bool reverse,is_root;   //區間反轉標記、是否是所在Splay的根
}T[maxn];


LCT中基本的Splay上操作:
int getson(int x){
	return x==T[T[x].fa].ch[1];
}
void pushreverse(int x){
	if(!x)return;
	swap(T[x].ch[0],T[x].ch[1]);
	T[x].reverse^=1;
}
void pushdown(int x){
	if(T[x].reverse){
		pushreverse(T[x].ch[0]);
		pushreverse(T[x].ch[1]);
		T[x].reverse=false;
	}
}
void rotate(int x){
	if(T[x].is_root)return;
	int k=getson(x),fa=T[x].fa;
	int fafa=T[fa].fa;
	pushdown(fa);pushdown(x);    //先要下傳標記
	T[fa].ch[k]=T[x].ch[k^1];
	if(T[x].ch[k^1])T[T[x].ch[k^1]].fa=fa;
	T[x].ch[k^1]=fa;
	T[fa].fa=x;
	T[x].fa=fafa;
	if(!T[fa].is_root)T[fafa].ch[fa==T[fafa].ch[1]]=x;
	else T[x].is_root=true,T[fa].is_root=false;
	//update(fa);update(x);    //如果維護了資訊,就要更新節點
}
void push(int x){
	if(!T[x].is_root)push(T[x].fa);
	pushdown(x);
}
void Splay(int x){
	push(x);   //在Splay到根之前,必須先傳完反轉標記
	for(int fa;!T[x].is_root;rotate(x)){
		if(!T[fa=T[x].fa].is_root){
			rotate((getson(x)==getson(fa))?fa:x);
		}
	}
}



access操作:

這是LCT最核心的操作。其他所有操作都要用到它。

他的含義是”訪問某節點“。作用是:對於訪問的節點x,打通一條從樹根(真實的LCT樹)到x的重鏈;如果x往下是重鏈,那麼把x往下的重邊改成輕邊。可以理解為專門開闢一條x到根的路徑,由一棵Splay維護這條路徑。

access之前:(粗的是重鏈)        access之後:

 

access實現的方式很簡單;

        先把x旋轉到所在Splay的根,然後把x的右孩子的is_root設為true(此時右孩子對應的是x下方的重鏈,這樣就斷開了x和下方的重鏈)。

        用y記錄上一次的x(初始化y=0),把y接到x的右孩子上,這樣就把上一次的重連結到了當前重鏈一起,同時記得T[y].is_root=false。

        記錄y=x,然後x=T[x].fa,把x上提。重複上面的步驟直到x=0。

程式碼:

void access(int x){
	int y=0;
	do{
		Splay(x);
		T[T[x].ch[1]].is_root=true;
		T[T[x].ch[1]=y].is_root=false;
		//update(x);    //如果維護了資訊記得更新。
		x=T[y=x].fa;
	}while(x);
}

mroot操作:

         這個操作的作用是把某個節點變成樹根(這裡的根指的是整棵LCT的根)。加上access操作,就可以方便的提取出LCT上兩點之間的路徑。提取u到v的路徑只需要mroot(u),access(v),然後v所在的Splay對應的鏈就是u到v的路徑。

mroot實現的方式:

         由於LCT是Splay組成的森林,所以要把x變成根就只需要讓所有Splay的父親最終指向x所在Splay。所以先access(x),Splay(x),把現在的根和將成為根的x鏈在一棵Splay中,並轉到根即可。但是我們注意到,由於x成為了新的根,所以它和原來的根所在的Splay中深度作為關鍵字的性質遭到了破壞:新根x應該是Splay中深度最小的,但是之前的操作並不會改變x的深度(也就是目前x依舊是當前Splay中深度最深的)。所以,我們需要把所在的這棵Splay翻轉過來。

(粗的是重鏈,y是原來的根)

翻轉前:                                                                      翻轉後:

 

這時候x才真正變成了根。

程式碼:

void mroot(int x){
	access(x);
	Splay(x);
	pushreverse(x);
}


link操作:

這個操作的作用是連線兩棵LCT。對於link(u,v),表示連線u所在的LCT和v所在的LCT;

link實現的方式:

很簡單,只需要先mroot(u),然後記錄T[u].fa=v就可以了,就是把一個Splay森林連到另一個上。

程式碼:

void link(int u,int v){
	mroot(u);
	T[u].fa=v;
}


cut操作:

         這個操作的作用是分離出兩棵LCT。

程式碼:

void cut(int u,int v)
	mroot(u);   //先把u變成根
	access(v);Splay(v);    //連線u、v
	pushdown(v);     //先下傳標記
	T[u].fa=T[v].ch[0]=0;
	//v的左孩子表示v上方相連的重鏈
	//update(v);  //記得維護資訊
}

這些就是LCT的基本操作。我推薦幾個LCT的練習題:

bzoj2049 SDOI2008洞穴勘探

模板題,只需要linkcut,然後詢問連通性。題解:

bzoj2002 HNOI2010彈飛綿羊

模板題,需要link和詢問某點到根的路徑長度。題解:

http://blog.csdn.net/saramanda/article/details/55210418

bzoj3669 NOI2014魔法森林

LCT的綜合應用。題解: