1. 程式人生 > 實用技巧 >【學習筆記】Link Cut Tree

【學習筆記】Link Cut Tree

Link Cut Tree(LCT) 是一種用來解決動態樹問題的資料結構。

前置芝士:Splay

部分參考:FlashHu 的部落格OI Wiki - Link Cut Tree

一、實鏈剖分和 LCT

實鏈剖分:將原樹的每條邊分成實邊虛邊。實邊中,兒子認父親,父親也認兒子;虛邊中,兒子認父親,但父親不認兒子。實邊相連得到實鏈。每條實鏈可用資料結構維護。每條邊的實虛是可變的,因此我們要採用更為高階的資料結構——Splay

LCT:用 Splay 來維護動態的樹鏈剖分,每條實鏈都用一個 Splay 維護。

LCT 可以幹這些事:

  1. 在兩點之間連一條邊
  2. 刪去兩點之間的邊
  3. 修改某個點/兩點間路徑的權值
  4. 查詢某個點的權值/兩點間路徑上的權值和/異或和/……
  5. 指定某點為原樹的根
  6. 判斷兩點之間是否連通
  7. ……

單次操作時間複雜度為 \(\operatorname{O}(\log n)\)

二、性質

主要有三點,後面許多操作都基於這些性質(下文出現的“深度”均指節點在原樹中的深度)。

  1. 每一個 Splay 維護的是一條從上往下、深度嚴格遞增的路徑;
  2. 每個節點包含且僅包含於一個 Splay中;
  3. 實邊包含在 Splay 中,而虛邊是由一棵 Splay 的指向該 Splay 中序遍歷最靠前的節點(最左邊的節點)在原樹中的父親(在原樹中這條鏈的頂端的父親)。

三、實現

1. 陣列含義(以 P3690 為例)

int ch[N][2];//兩個兒子
int val[N];//該節點的權值
int xs[N];//子樹權值異或和
int fa[N];//父親
int tag[N];//翻轉標記
int stk[N];//陣列模擬棧

2. Splay 基本操作

這些都是 Splay 基本操作。其中 splay 與平常我們寫的有些不同,需要注意。

還有一個 notrt 函式,作用是:判斷一個點 p 是否不是 p 所在 splay 的根,只需要判斷是否和父親實邊相連(p 的父親是否認 p)

inline int ident(int p) {
	return ch[fa[p]][1] == p;
}

inline void update(int p) {
	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
}

inline bool notrt(int p) {
	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
}

inline void connect(int p, int f, int cc) {//父子相認,是實邊
	fa[p] = f;
	ch[f][cc] = p;
}

inline void flip(int p) {//翻轉函式,打上標記並交換這個節點的左右兒子
	if(!p) return;
	tag[p] ^= 1;//0^1=1,1^1=0 即若原來無則現在有,若原來有則現在無(區間翻轉的性質)
	swap(ch[p][0], ch[p][1]);
}

inline void push(int p) {//下傳標記,清空該節點的標記並傳給兩個兒子
	if(!tag[p]) return;
	tag[p] = 0;
	flip(ch[p][0]);
	flip(ch[p][1]);
}
inline void rotate(int p) {
	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
	fa[p] = r;
	if(notrt(q)) ch[r][cq] = p;//特別要注意,若notrt不為真說明q-r是一條虛邊,旋轉之後應該還是虛邊,不能認兒子
	connect(w, q, cp);
	connect(q, p, cp ^ 1);
	update(q);
}

inline void splay(int p) {
	int top, q;
	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
	while(top) push(stk[top--]);//從上至下把根到這個點的路徑上所有點的標記下傳
	for(; notrt(p); rotate(p))//這裡要判notrt,保證在當前Splay內
		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
	update(p);
}

3. LCT 基本操作

  • \(\text{access(p)}\)

作用:把 p 到根這條路徑上的邊全部變為實邊,構成一棵Splay。

由於性質 1,該路徑上其它鏈都要給這條鏈讓路,也就是把路徑上的每個點到該路徑以外的點之間的實邊變成虛邊。

由於性質 3,虛邊一定連結著一棵 Splay 的根。所以要先把 p 轉到該 Splay 的根。

假設在轉之前,p 和 q 之間有虛邊相連(q 是一棵深度比 p 大的Splay 的根),說明 q 的父親是 p,那麼直接 ch[p][1]=q 即可。兒子變了,要更新節點資訊。

然後 q=p,p 則向上爬,變為它的父親,進入下一個 Splay。然後重複上述操作。

簡而言之:

  1. 旋轉到根
  2. 換右兒子
  3. 更新資訊
  4. 向上爪巴

下面來模擬一下這個過程(由於筆者又菜又懶,直接用了 FlashHu 的文章中的圖片):

\(\texttt{access(N)}\)

inline void access(int p) {
	for(int q = 0; p; p = fa[p]) {
		splay(p);
		ch[p][1] = q;
		update(q = p);//右兒子改變,需要更新
	}
}
  • \(\text{make_root(p)}\)

作用:把 p 變成原樹中的根。

想讓p成為原樹中的根,就要讓它與現在的根之間的路徑全為實邊。

access(p) 後,p 一定是 p 所在 Splay 中深度最大的點。因為 access 中第一遍迴圈 q=0ch[p][1]=qch[p][1]=0。也就是說右子樹為空,根據性質 1 可知該 Splay 中無比 p 深度更大的節點。

根是樹中深度最小的節點,但它是深度最大的,所以要將 p 所在的 Splay 整個翻轉(使用類似線段樹的懶標記)。

inline void make_root(int p) {
	access(p);
	splay(p);
	flip(p);
}
  • \(\text{find_root(p)}\)

作用:尋找x所在原樹的樹根,常用於判斷連通性。

首先 access(p),讓 p 和 root 處於同一 Splay。

由於根節點深度最小,它一定在 Splay 的最左邊(中序遍歷中最前面的節點)。把 p 旋到根,一直向左走直到沒有左兒子。這個節點就是根節點。最後 splay(p) 保證複雜度正確。

inline int find_root(int p) {
	access(p);
	splay(p);
	for(; ch[p][0]; p = ch[p][0]) push(p);
	splay(p);
	return p;
}
  • \(\text{split(p,q)}\)

作用:將路徑 p-q 上的所有邊變為實邊,成為一個Splay。

之前已經實現過 access,可以將原樹的根到某節點的路徑上的所有邊變為實邊,成為一個Splay。make_root(p),p就變成了原樹的根節點。將路徑 p-q 上的所有邊變為實邊就轉化為將原樹的根到q的路徑上的所有邊變為實邊。

inline void split(int p, int q) {
	make_root(p);
	access(q);
	splay(q);//此時splay的根為q,整個鏈的資訊可以直接從q獲取
}
  • \(\text{link(p,q)}\)

作用:在 pq 之間連一條邊。

原樹的根節點是沒有(虛邊相連的)父親的,那麼讓 p 成為原樹根,父親指向 q 就連線了 p 和 q。

inline void link(int p, int q) {
	make_root(p);
	if(find_root(p) ^ find_root(q)) fa[p] = q;//fdrt(p)=fdrt(q)說明兩點聯通,再連邊不合法
	//此時這條邊是虛邊,不在同一splay,不能update
}
  • \(\text{cut(p,q)}\)

作用:斷開連線 pq 的邊。

首先判斷 p,q 是否有邊。 make_root(p),p 成為原樹的根,深度最小。所以 q 若與 p 相連,q 一定在 p 的右子樹內。由於性質 1,這棵 Splay 中序遍歷中 p 和 q 一定要相鄰才行。那就有三種情況:

  1. p,q不連通
  2. p,q連通,但q的父親不是p
  3. q的左子樹不為空
inline void cut(int p, int q) {
	make_root(p);
	if(find_root(q) ^ p || fa[q] ^ p || ch[q][0]) return;
	fa[q] = ch[p][1] = 0;
	update(p);
}

四、其他神奇的操作

咕咕咕

五、例題

模板題,所有操作前文已講,直接上程式碼。

#include <cstdio>
#include <cstring>

using namespace std;

#define in inline
typedef long long ll;
in int max(int x, int y) {return x > y ? x : y;}
in int min(int x, int y) {return x < y ? x : y;}
in void swap(int &x, int &y) {x ^= y ^= x ^= y;}
#define rei register int
#define rep(i, l, r) for(rei i = l, i##end = r; i <= i##end; ++i)
#define repd(i, r, l) for(rei i = r, i##end = l; i >= i##end; --i)
char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
#define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
in int read() {
	int res = 0; char ch = getchar(); bool f = true;
	for(; ch < '0' || ch > '9'; ch = getchar())
		if(ch == '-') f = false;
	for(; ch >= '0' && ch <= '9'; ch = getchar())
		res = res * 10 + (ch ^ 48);
	return f ? res : -res;
}
const int N = 1e5 + 15;

int ch[N][2], val[N], xs[N], fa[N], tag[N], stk[N], tot;

in int ident(int p) {
	return ch[fa[p]][1] == p;
}

in void update(int p) {
	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
}

in void connect(int p, int f, int cc) {
	fa[p] = f;
	ch[f][cc] = p;
}

in void flip(int p) {
	if(!p) return;
	tag[p] ^= 1;
	swap(ch[p][0], ch[p][1]);
}

in void push(int p) {
	if(!tag[p]) return;
	tag[p] = 0;
	flip(ch[p][0]);
	flip(ch[p][1]);
}

in bool notrt(int p) {
	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
}

in void rotate(int p) {
	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
	fa[p] = r;
	if(notrt(q)) ch[r][cq] = p;
	connect(w, q, cp);
	connect(q, p, cp ^ 1);
	update(q);
}

in void splay(int p) {
	int top, q;
	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
	while(top) push(stk[top--]);
	for(; notrt(p); rotate(p))
		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
	update(p);
}

in void access(int p) {
	for(int q = 0; p; p = fa[p]) {
		splay(p);
		ch[p][1] = q;
		update(q = p);
	}
}

in void mkrt(int p) {
	access(p);
	splay(p);
	flip(p);
}

in int fdrt(int p) {
	access(p);
	splay(p);
	for(; ch[p][0]; p = ch[p][0]) push(p);
	splay(p);
	return p;
}

in void split(int p, int q) {
	mkrt(p);
	access(q);
	splay(q);
}

in void link(int p, int q) {
	mkrt(p);
	if(fdrt(p) ^ fdrt(q)) fa[p] = q;
}

in void cut(int p, int q) {
	mkrt(p);
	if(fdrt(q) ^ p || fa[q] ^ p || ch[q][0]) return;
	fa[q] = ch[p][1] = 0;
	update(p);
}

signed main() {
	int n = read(), q = read(), opt, x, y;
	rep(i, 1, n) val[i] = read();
	for(; q; --q) {
		opt = read(); x = read(); y = read();
		switch(opt) {
			case 0 : split(x, y); printf("%d\n", xs[y]); break;
			case 1 : link(x, y); break;
			case 2 : cut(x, y); break;
			case 3 : splay(x); val[x] = y; break;
		}
	}
	return 0;
}

咕咕咕