[學習筆記 #7] Link Cut Tree

huangkxQwQ發表於2024-12-06

目錄
  • [學習筆記 #7] Link Cut Tree
    • LCT 的基礎
      • 作用、優勢 和 劣勢
      • 結構
      • 基本操作
        • PushUp
        • Reverse
        • PushDown
        • IsRoot
        • PushDownAll
        • Which
        • Rotate
        • Splay
        • Access
        • MakeRoot
        • FindRoot
        • Link & Cut & Split
        • 單點修改 & 單點查詢 & 鏈修改 & 鏈查詢
      • 維護邊的資訊
      • 維護子樹資訊
      • 洛谷上的模板題的程式碼
    • LCT 的應用
      • [動態樹問題]
      • 維護最小生成樹 & [瓶頸生成樹]
      • 維護連通塊個數
      • 維護樹的直徑(直徑端點)
      • 維護樹的重心
      • 維護(無向圖的)割邊
      • 維護(無向圖的)割點
      • with SAM
      • with DP

[學習筆記 #7] Link Cut Tree

重學 LCT,然後被 hfu 說了。於是稍微寫篇學習筆記就走人。

[ ] 裡的是我還不確定的。

LCT 的基礎

作用、優勢 和 劣勢

LCT 是一種解決動態樹問題的資料結構。它可以維護森林,顯著的優勢是可以支援加邊和刪邊,而劣勢在於維護子樹資訊並不方便。

結構

  • LCT 維護 森林。
  • LCT 由若干棵 Splay 構成。
    • 每棵 Splay 維護一條鏈。
    • 一些 Splay 連在一起 維護 一棵樹。
  • 統一寫到一起而不是分開寫成若干個板塊。

具體地,類似樹鏈剖分,Splay 也將樹剖分成若干條鏈,但是為了動態,鏈是實鏈而不是重鏈。

每個結點選擇一個子結點作為實兒子,它的其他子結點作為虛兒子;[某 [個 / 些] 非葉子結點也可能會不選實兒子,此時它的子結點都是虛兒子]。

把每個點連向實兒子的邊稱為實邊,把每個點連向虛兒子的邊稱為虛邊。

實邊連成的鏈叫實鏈,對每條實鏈用一個 Splay 維護。特殊地,每個落單的結點都是一條實鏈。

虛邊連線實鏈。

每棵 Splay 維護的鏈以深度從小到大為序。

虛邊連線兩棵(多棵)Splay 的方式是“認父不認子”,即要把原樹上這條邊所連子結點的 fa 設為父結點,但父結點不把子結點納入它的 ch 中(ch 維護的是 Splay 上的子結點)。

基本操作

實現方法不止一種,這裡記我現在用的寫法。

一些需要說明的:

  • Splay 的根的左子樹是這條鏈上它上面的結點,右子樹是這條鏈上它下面的結點。
  • 注意哪些地方改了邊的虛實。
  • 注意修改的地方是否要寫 PushDown 和 PushUp。
  • 我這裡 PushDown 的寫法是類似我平時寫線段樹的那種,即 PushDown 這個結點之前已經處理了這個結點的翻轉(可能交換了這個結點的左右兒子)。
  • LCT 不管更不維護原樹長什麼樣。
  • LCT 維護的權值在點上。
  • 其他見程式碼。

PushUp

合併資訊,上傳。

類似平衡樹,要管自己這個結點上的資訊。

Reverse

翻轉。

交換 Splay 上某個結點的左右兒子,並給翻轉標記異或 \(1\)

PushDown

下放標記。

類似線段樹 [、平衡樹],如果翻轉標記是 \(1\) 就讓左右兒子都 Reverse,將翻轉標記設為 \(0\)(即清除標記)。

IsRoot

判斷一個點是否是 原森林裡的樹根

根據“認父不認子”判一下即可。

PushDownAll

[[[為 Splay(操作名)做準備]]]。一個點不斷往上跳,直到到了 它所在原樹的根,從上往下 PushDown 回來。

遞迴寫起來很好看,見程式碼。

Which

使 Rotate 寫起來更簡潔。判斷一個點是它 在 Splay 上的父親 的左兒子還是右兒子。

直接判,見程式碼。

Rotate

左旋右旋二合一,把一個點往上旋一層。

畫圖理解,理清順序,不要漏掉結點,不要把一個東西改了還以為用的是原來的值,不要忘記 PushUp……注意細節。寫法和其他要注意的見程式碼。

Splay

把一個點旋轉成它所在 Splay 的根。

見程式碼。可能需要背一下。

“不需要背,記住就行了。”(好像是這麼說的)

Access

把一個點到 它所在原樹的根 的鏈變成實鏈,並且把這條鏈上的點連著的其他所有邊都變成虛邊

不斷 Splay,刪邊並連邊,直到處理完原樹的根。刪邊和連邊透過改右兒子來一起實現,要 PushUp。見程式碼。

MakeRoot

把一個點變成 它所在原樹 的根。

需要把這個點到 它所在原樹的根 的鏈反向。Access, Splay, Reverse。

FindRoot

找一個點所在 原樹 的根。

Access, Splay,不斷向左兒子跳,跳不動就找到了。[要 Splay 上來保證複雜度]。

Link:(可以先判是否連通)新增一條原樹上的邊。

Cut:(可以先判是否有這條邊)刪除一條原樹上的邊。

Split:把一條鏈作為一棵 Splay 取出來,並且為了方便同時把這條鏈的一個端點旋到這棵 Splay 的根。

都見程式碼。

單點修改 & 單點查詢 & 鏈修改 & 鏈查詢

單點的改要先 Splay 上去,防止直接大力往上 PushUp([這樣複雜度不對])。

鏈的直接 Split 出來,再操作。

都見程式碼。

維護邊的資訊

給原來的每條邊新開一個點即可。

維護子樹資訊

兩種。

實兒子的子樹資訊顯然在 Splay(資料結構)中的 PushUp 就可以上傳。那麼還要統計虛兒子的子樹資訊。

對每個結點開一個值來記虛兒子的子樹資訊的 [並],[需要更改這個值的地方是對邊的虛實有修改的地方]。

注意到修改既需要新增也需要刪除。

  • 如果資訊有可減性,直接做。
  • 如果資訊沒有可減性,需要用支援需要支援的刪除的資料結構,比如平衡樹。可以用 set,[會乘一隻 \(\log\)];手寫 Splay [不會乘那隻 \(\log\)]。

洛谷上的模板題的程式碼

#include <bits/stdc++.h>
#define gc getchar
#define pc putchar
using namespace std;

namespace FastIO{
	int rd()
	{
		int x = 0, f = 1; char c = gc();
		while(c < '0' || c > '9') { if(c == '-') f = (- 1); c = gc(); }
		while(c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); }
		return (x * f);
	}
	void wt(int x)
	{
		if(x < 0) { x = (- x); pc('-'); }
		if(x > 9) wt(x / 10);
		pc(x % 10 + '0');
	}
}
using namespace FastIO;

const int N = 1e5;

namespace LCT{
	int fa[N + 1], ch[N + 1][2], val[N + 1], s[N + 1], rev[N + 1];
	void PushUp(int u) { s[u] = s[ch[u][0]] ^ val[u] ^ s[ch[u][1]]; } // s[0] == 0
	void Reverse(int u) { rev[u] ^= 1; swap(ch[u][0], ch[u][1]); }
	void PushDown(int u) { if(rev[u]) Reverse(ch[u][0]), Reverse(ch[u][1]), rev[u] = 0; } // ch == 0 也沒有關係
	bool IsRoot(int u) { return u != ch[fa[u]][0] && u != ch[fa[u]][1]; }
	void PushDownAll(int u) { if(! IsRoot(u)) PushDownAll(fa[u]); PushDown(u); }
	int Which(int u) { return u == ch[fa[u]][1]; }
	void Rotate(int u) { int v = fa[u], w = fa[fa[u]], wu = Which(u), wv = Which(v); if(! IsRoot(v)) ch[w][wv] = u; ch[v][wu] = ch[u][wu ^ 1]; fa[v] = u; if(ch[u][wu ^ 1]) fa[ch[u][wu ^ 1]] = v; fa[u] = w; ch[u][wu ^ 1] = v; PushUp(v); PushUp(u); }
	// Rotate: 先 PushUp v 再 PushUp u;用 ! IsRoot(v),而不是直接用 w;ch[v][wu],而不是 ch[v][wu ^ 1];畫圖!檢查!
//	inline void Rotate(int x) { int y = fa[x], z = fa[y], wx = Which(x), wy = Which(y); if(! IsRoot(y)) ch[z][wy] = x; fa[x] = z; ch[y][wx] = ch[x][wx ^ 1]; if(ch[x][wx ^ 1]) fa[ch[x][wx ^ 1]] = y; ch[x][wx ^ 1] = y; fa[y] = x; PushUp(y); PushUp(x); }
	void Splay(int u) { PushDownAll(u); while(! IsRoot(u)) { if(! IsRoot(fa[u])) Rotate(Which(u) == Which(fa[u]) ? fa[u] : u); Rotate(u); } } // PushDownAll // 第一個 Rotate 裡的。 //
	int Access(int u) { int v; for(v = 0; u; v = u, u = fa[u]) { Splay(u); ch[u][1] = v; PushUp(u); } return v; } // int ? // PushUp // u; //
	void MakeRoot(int u) { Access(u); Splay(u); Reverse(u); } //
	int FindRoot(int u) { Access(u); Splay(u); while(ch[u][0]) PushDown(u), u = ch[u][0]; Splay(u); return u; } // Splay
	void Link(int u, int v) { MakeRoot(u); if(FindRoot(v) != u) fa[u] = v; }
	void Cut(int u, int v) { MakeRoot(u); Access(v); Splay(v); if(ch[v][0] == u && ch[u][1] == 0) { ch[v][0] = fa[u] = 0; PushUp(v); } } // PushUp //
	void Split(int u, int v) { MakeRoot(u); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ Access(v); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ Splay(u); }
	int Query(int u, int v) { Split(u, v); /*wt(ch[u][0]), pc(' '), wt(ch[u][1]), pc('\n');*/ return s[u]; }
	void Modify(int u, int Val) { Splay(u); val[u] = Val; PushUp(u); }
}
using namespace LCT;
// LCT 中有的函式(比如 FindRoot 和 Access 有額外的功能)
// 編譯錯誤不知道什麼問題的時候先把 namespace LCT(三行,有一行是括號)註釋掉。

void Solve()
{
	int n = rd(), m = rd();
	for(int i = 1; i <= n; ++ i) { int Val = rd(); Modify(i, Val); }
	while(m --){
		int op = rd(), x = rd(), y = rd();
		if(op == 0) wt(Query(x, y)), pc('\n');
		else if(op == 1) Link(x, y);
		else if(op == 2) Cut(x, y);
		else Modify(x, y);
	}
}

int main()
{
	Solve();
	return 0;
}
// 注意題目中操作型別的編號從 0 開始。

LCT 的應用

動態維護一些東西。

[動態樹問題]

維護最小生成樹 & [瓶頸生成樹]

可以支援動態加邊,不能刪邊。只有刪邊的可以轉換成反著加邊。

可以透過維護 [瓶頸生成樹] 來維護“有序的連通性”。

連邊時,不連通的直接連線,連通的看要不要替換,找到路徑裡最劣的邊,看替換掉它會不會更優,更優就替換(Cut、Link)。

找路徑裡最劣的邊、Cut、Link 可以用 LCT 維護。

維護連通塊個數

連通塊用生成樹來記(對每個連通塊求一棵生成樹)。連通塊的個數等於 總點數 減去 所有生成樹的邊數之和。

如果是多組詢問求編號在一段區間裡的邊形成的連通塊的個數,就用掃描線,令生成樹為按編號的 [瓶頸生成樹]。

LCT 維護生成樹,只保留了儘量存在於區間中的生成樹裡的邊。

那麼連通塊的個數就是 總點數 減去 LCT 中的且在區間中的邊的個數。

LCT 刪邊加邊都是一條條加、刪的。

這就轉化為了二維數點,數有多少條 生成樹中的邊,滿足它的編號在區間內。

維護樹的直徑(直徑端點)

用好直徑的性質。

當我們合併兩個連通塊時,得到的新直徑端點一定是原來 44 個點中的兩個,兩兩求距離更新即可。儲存連通塊的直徑端點可以考慮並查集——from《從板子到黑題的LCT - 洛谷專欄 (luogu.com.cn)

維護樹的重心

【洛谷4299】首都(LCT維護樹的重心) - TheLostWeak - 部落格園 (cnblogs.com)

題解 P4299 首都 - 洛谷專欄 (luogu.com.cn)

  1. 兩棵樹合併時,新的重心必定在兩棵樹的重心間的路徑上。

  2. 重心的各子樹大小都小於等於整棵樹的一半。

用 LCT 取出這條路徑,在 Splay 上二分,二分完記得 Splay 保證時間複雜度。需要用 LCT 維護子樹資訊。

具體的二分方式我還不會。

維護(無向圖的)割邊

類似維護生成樹,不連通的直接連,連通的(形成環)要縮點。形成環時:

  • 可以不真正縮點,而是賦邊權。邊權為 \(1\) 表示是割邊,為 \(0\) 表示不是。加邊形成環就把樹上這條鏈的邊權都賦成 \(0\),正確性我忘了。(維護生成樹。)
  • 可以真正縮點。

維護(無向圖的)割點

類似維護生成樹,不連通的直接連,連通的(形成環時)因為一個割點可能屬於多個點雙,所以沒法縮點,於是維護“圓方樹”。形成環時:

  • 新建一個方點,把那條鏈上的邊都刪了,連到這個方點上。
  • [[[這裡的圓方樹允許圓點和圓點、方點和方點相連。]]]
  • 關於時間複雜度的感性理解:把一條鏈拆成了菊花(原來鏈上任意兩點,它們之間的距離現在都是 \(2\) 了)。

with SAM

還不會,咕咕咕。

with DP

P4115 Qtree4 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)

參考(學習):

  • 從板子到黑題的LCT - 洛谷專欄 (luogu.com.cn)。(包括這篇部落格裡給的題單)
  • 【洛谷4299】首都(LCT維護樹的重心) - TheLostWeak - 部落格園 (cnblogs.com)
  • 題解 P4299 首都 - 洛谷專欄 (luogu.com.cn)
  • 動態樹練習題 - 題單 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)。(另一個題單)
  • Link Cut Tree - OI Wiki (oi-wiki.org)

2024.12.6

相關文章