diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 2a4d3da4..215b6791 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -10,6 +10,8 @@ - [Basic Greedy](greedy/basic.md) - [Dynamic Programming]() - [Basic DP]() +- [Data Structure]() + - [Link-Cut Tree](data_structure/link_cut_tree.md) - [Flow]() - [Sqrt Technique](sqrt/intro.md) - [Square Root Decomposition](sqrt/sqrt_decomposition.md) diff --git a/src/data_structure/image/LCT/Auxiliary_Tree_Demo.png b/src/data_structure/image/LCT/Auxiliary_Tree_Demo.png new file mode 100644 index 00000000..79f11071 Binary files /dev/null and b/src/data_structure/image/LCT/Auxiliary_Tree_Demo.png differ diff --git a/src/data_structure/image/LCT/access_demo.gif b/src/data_structure/image/LCT/access_demo.gif new file mode 100644 index 00000000..d9ec8331 Binary files /dev/null and b/src/data_structure/image/LCT/access_demo.gif differ diff --git a/src/data_structure/image/LCT/access_demo.png b/src/data_structure/image/LCT/access_demo.png new file mode 100644 index 00000000..a1125565 Binary files /dev/null and b/src/data_structure/image/LCT/access_demo.png differ diff --git a/src/data_structure/image/LCT/cut_edge.png b/src/data_structure/image/LCT/cut_edge.png new file mode 100644 index 00000000..e64d683c Binary files /dev/null and b/src/data_structure/image/LCT/cut_edge.png differ diff --git a/src/data_structure/link_cut_tree.md b/src/data_structure/link_cut_tree.md new file mode 100644 index 00000000..b1a168d2 --- /dev/null +++ b/src/data_structure/link_cut_tree.md @@ -0,0 +1,851 @@ +# Link-Cut Tree + +## 介紹 + +Link-Cut Tree 是一種樹狀的資料結構,具體來說是一個森林,也就是很多樹的集合,Link-Cut Tree 支援以下操作: + +- 在兩個點之間建立一條邊 +- 在兩個點之間斷開一條邊 +- 查詢兩點之間是否存連通 + +Link-Cut Tree 是以 Splay Tree 為基礎實作,因此尚未了解 Splay Tree 可以先回去參考 Splay Tree,本篇著重於介紹 Link-Cut tree。 + +以下文章將用 LCT 簡稱 Link-Cut Tree。 + +## 先備知識 + +- Splay Tree +Splay Tree 是一種自平衡的二元搜尋樹,主要通過 Splay 操作讓最近被訪問的節點移動至樹的根部,並且 Splay 操作的時間複雜度為均攤 \\(O(\log n)\\)。 + +- 輕重鏈剖分 +輕重鏈剖分用來處理樹上的動態查詢,它的精髓在於將樹拆分成很多條鏈,使得樹上的操作可以有好的時間複雜度,LCT 也有使用到類似的技巧,因此建議先理解輕重鏈剖分後再回來看 LCT。 + +## 名詞定義 + +### 代表樹 + +代表樹就是原樹,原樹由一般的邊以及特別的邊 preferred edge 所連接,在進行 LCT 的 ``access()`` 操作時,會把走訪的邊都設為 preferred edge,並且把 preferred edge 所連接到的兒子稱為 preferred child,一條全部由 preferred edge 所構成的 path 則稱為 preferred path。 + +### Auxiliary Tree + +以下使用輔助樹來稱呼 Auxiliary Tree。 + +要維護 LCT 的操作,我們需要維護輔助樹 (Auxiliary Tree)。輔助樹通常使用 splay tree 來實作,輔助樹的作用是用來維護 preferred edge,同時也會維護輔助樹的訊息,因此輔助樹能維護的訊息,就決定了 LCT 可以維護的訊息。 +每一棵輔助樹都對應到原樹上的一條 preferred edge,而不同的輔助樹之間由 path-parent pointer 連結,這種邊只會由兒子指向父親,而父親不會指向兒子。 +以下是一個原樹與輔助樹的對應關係: +圖解:左為原樹,原樹的粗邊代表 preferred edge。右為原樹所對應的輔助樹,一般的邊代表 splay node 的左右小孩,而帶有箭號的邊代表 path-parent pointer,用來連接不同的輔助樹。 +在原樹中 ABD 構成一條 preferred path,因此 ABD 在同一個輔助樹中,並且依照原樹的深度進行平衡,除此之外 CG 也構成一條 preferred path,因此 CG 在同一個輔助樹中,最後根據原樹上的邊,將輔助樹彼此用 path-parent pointer 連接。 + +在基本 LCT 的 Splay Tree 節點會維護以下訊息: + +1. 父節點 +2. 左右小孩 (實作時用右小孩代表 preferred edge) +3. 在 Splay Tree Node 左邊的節點深度比自己小,右邊的節點深度比自己大 +4. 因為 LCT 操作中要維護左右節點深度的性質,所以在某些特定的操作中需要將區間反轉,因此使用懶惰標記,讓翻轉區間能夠有好的時間複雜度 + +以下是一個簡單的 Splay Tree node: + +```cpp +struct splay_node +{ + int child[2], pa; + bool rev; + splay_node() : pa(0), rev(false), child({0, 0}) {} +}; +``` + +- ``child[0]`` 代表左小孩(以下用 lc 表示)、``child[1]`` 代表右小孩 (以下用 rc 表示) +- ``parent`` 代表父親 +- ``rev`` 代表區間反轉的懶惰標記 + +接下來就是這棵輔助樹的基本操作。 + +#### ``splay()`` 以及 ``rotate()`` + +Splay Tree 的基本操作。 + +```cpp +void rotate(int x) // balance splay tree +{ + int y = cur.pa, z = node[y].pa, d = (node[y].rc == x); + cur.pa = z; + if (!isroot(y)) + node[z].child[node[z].rc == y] = x; + node[y].child[d] = cur.child[d ^ 1]; + node[node[y].child[d]].pa = y; + node[y].pa = x, cur.child[d ^ 1] = y; + up(y); + up(x); +} +void splay(int x) // splay node x +{ + push_down(x); + while (!isroot(x)) + { + int y = cur.pa; + if (!isroot(y)) + { + int z = node[y].pa; + if ((node[z].lc == y) ^ (node[y].lc == x)) + rotate(x); + else + rotate(y); + } + rotate(x); + } +} +``` + +這裡的 ``splay()`` 寫法跟一般的 Splay Tree 不太一樣,LCT 中的 splay 只要到當前這棵輔助樹的樹根即可,因此需要用到 ``isroot()`` 來判斷。 + +#### ``isroot()`` + +判斷當前節點是否為整棵樹的根。 + +```cpp +bool isroot(int x) +{ + return node[cur.pa].lc != x && node[cur.pa].rc != x; +} +``` + +如果父節點的左小孩跟右小孩都不是自己,就代表自己是輔助樹的根,換一種說法就是自己與父節點相連的邊不是 preferred edge。 + +- ``push_down()`` +遞迴將祖先的懶惰標記往下推。 + +```cpp +void push_down(int x) +{ + if (!isroot(x)) + push_down(cur.pa); + down(x); +} +``` + +- ``down()`` +真正在做懶惰標記下推的部分。 + +```cpp +void down(int x) +{ + if (cur.rev) + { + swap(cur.lc, cur.rc); + if (cur.lc) + node[cur.lc].rev ^= 1; + if (cur.rc) + node[cur.rc].rev ^= 1; + cur.rev = 0; + } +} +``` + +- ``up()`` +``up()`` 可以將子節點的訊息向上更新,可以自行修改成紀錄其他訊息,例如紀錄子樹大小。 +最基本的 LCT 沒有使用到。 + +### LCT 基本操作 + +#### ``access()`` + +``access()`` 是 **LCT 中最重要的函式**,可以把當前節點到 LCT 根結點上面所有的邊變成 preferred edge。 +操作方法: + +1. 把當前節點 splay 到目前輔助樹的根 +2. 把當前節點的 preferred child 設定為上一次走到的節點 (將右節點設為上一次走到的點) +3. 維護節點訊息 +4. 對父節點進行 ``access()`` +重複執行 1~4,直到抵達整棵樹的根結點回傳。 + +```cpp +int access(int x) +{ + int last = 0; + for (; x; last = x, x = cur.pa) + { + splay(x); + cur.rc = last; + up(x); + } + return last; +} +``` + +操作完成後 ``x`` 節點會與根結點存在同一棵輔助樹中。 + +以下用一個例子來展示 ``access()``: + +假設要執行的操作是 ``access(F)``,一開始要先 ``splay(F)``,讓 F 節點變成當前輔助樹的根。``splay(F)`` 後,要把節點的 preferred edge 設定為上次走到的節點,因為 F 是第一個節點,因此不需要動作,接下來繼續往上層更新,對父節點進行 ``access()``,因此接下來要 ``access(C)``。 +先 ``splay(C)``,讓 C 變成輔助樹的根節點。把 C 的 preferred edge 設為 F (這裡為了展示所以將新的 preferred edge 變成粗邊),所以 C 節點拋棄其中一個小孩,這裡展示的是拋棄 B 小孩。最終 ``access(A)``,到達整棵樹的根結點,因此停止操作,最終 F 與根節點 A 的路徑為 preferred path,且 F 與 A 在同一棵輔助樹中。 + +上圖是 ``access(F)`` 的前後對照圖。 + +#### ``make_root()`` + +將當前節點變為整棵樹的 root。操作方法: + +1. 先利用 ``access()`` 將當前節點到根節點的邊都變成 preferred edge +2. ``splay()`` 當前節點,使他變成當前輔助樹的根 +當完成此操作後,會造成此節點到原本的根的路徑全部反轉,為了要維護**左邊的節點深度比自己小,右邊的節點深度比自己大**這個性質,必須將此區間反轉。 +3. 變更當前節點的懶惰標記 ``rev`` + +```cpp +void make_root(int x) +{ + access(x); + splay(x); + cur.rev ^= 1; +} +``` + +#### ``link()`` + +將兩個節點所在的樹合併,兩個節點必須在不同樹。操作方式: + +1. 讓其中一個節點 ``x`` 變成根結點 +2. 將 ``x`` 節點的父節點設為另一個節點 + +```cpp +void link(int x, int y) +{ + make_root(x); + cur.pa = y; +} +``` + +#### ``cut()`` + +斷開兩個節點之間的邊,兩個節點之間必須有邊。操作方式: + +1. 將其中一個節點 ``x`` 變成根結點 +2. ``access()`` 另一個節點 ``y``,讓 ``x`` 和 ``y`` 在同一個輔助樹中 +3. ``splay()`` 節點 ``y``,讓他變成輔助樹的根 +此時 ``x`` 節點會成為 ``y`` 節點的左小孩 +4. 斷開 ``x`` , ``y`` 節點之間的連結 + +```cpp +void cut(int x, int y) +{ + make_root(x); + access(y); + splay(y); + node[y].lc = 0; + cur.pa = 0; +} +``` + +另一種 ``cut()`` 操作是針對單一節點,在原樹中斷開該節點與父節點的邊。操作方式: + +1. 直接 ``access()`` 該節點,讓他與當前的根在同一個輔助樹中 +2. ``splay()`` 該節點,讓該節點變成新的根結點 +此時父節點位於左小孩的位置 +3. 斷開該節點與父節點的連結 + +```cpp +void cut(int x) +{ + access(x); + splay(x); + node[cur.lc].pa = 0; + cur.lc = 0; +} +``` + +#### ``find_root()`` + +尋找此節點所在樹的根結點。操作方式: + +1. 首先 ``access()`` 此節點,讓他和根結點在同一個輔助樹裡面 +2. 根據 LCT 的性質,只要找到最左邊的節點代表深度最小,也就是根 +3. 最後記得 ``splay()`` 找到的根 + +```cpp +int find_root(int x) +{ + int res = access(x); + while (node[res].lc) + res = node[res].lc; + splay(res); + return res; +} +``` + +### 各操作時間複雜度 + +LCT 的操作中,都是基於 ``access()`` 操作而完成的,因此只要能分析出 ``access()`` 複雜度就可以知道各個操作的時間複雜度。 + +首先 ``access()`` 是由 ``splay()`` 以及迴圈所構成,``splay()`` 操作的時間複雜度是 \\( O(\log N) \\),接下來只要分析出 ``splay()`` 會被執行幾次,也就是 preferred child 的改變數量,就可以知道時間複雜度了。 + +這裡引入 Heavy-light decomposition 的定義,heavy edge 代表所連接的子節點位於最大的子樹中,light edge 代表所連接的子節點不是最大的子樹,在 LCT 中,每個節點只會有兩個子節點,因此 heavy edge 所連接的子節點具有以下性質:\\( size(子節點) > 1/2 size (父節點) \\),因此我們將邊分為以下四種 heavy-prefered, heavy-unpreferred, light-preferred, light-unpreferred。 + +在每一次 ``splay()`` 後都會有一個邊被設為 preferred child,如果被設為 prefered child 的邊是 light edge,則這條路徑上最多只會有 \\(\log n\\)條 light edge,因此 ``splay()`` 最多只會被執行 \\(\log n\\)次,接下來討論如果被設為 prefered child 的邊是 heavy edge,因為最多有 \\(n - 1\\)條 heavy edge,因此在最糟的情況下,``splay()`` 會被執行 \\(n - 1\\)次,但在每次 ``access()`` 中,最多只有 \\(\log n\\)條邊從 light-unpreferred edge 變成 light-preferred edge,因此最多只有 \\(\log n\\)條邊從 heavy-preferede edge 變成 heavy-unprefered edge,假設有 \\(m\\) 次操作,花在 heavy edge 上的操作最多為 \\(m(\log n) + (n - 1)\\),在足夠次數的操作時 (\\(m > n - 1\\)),時間複雜度為 \\(O(\log N) \\)。 + +目前可以得出,最多會有 \\(O(\log N) \\) 次 ``splay()``,因此 ``access()`` 的上界變成了 \\( O(\log^2 N) \\),``splay()`` 執行的次數等價於 preferred child 的改變數量,因此只要能夠讓改變的數量變成均攤 \\(O(1)\\),就可以把 ``access()`` 的時間複雜度變為 \\(O(\log N) \\)。 + +利用 potential method,假設 \\(s(v)\\) 代表在 \\(v\\) 節點下的子樹大小,所以 potential function 就可以寫成 \\(\Phi = \sum_{v} \log s(v) \\),根據 splay tree 的均攤分析,\\(cost(splay(v)) \leq 3(\log s(root(v)) - \log s(v)) + 1 \\),假設在 ``splay()`` 後,\\(v\\) 是 \\(w\\) 的子節點,可以得到\\(s(v) \leq s(w)\\),最終把 ``splay()`` 的操作與 preferred child 的改變數量加起來,得到\\(3(\log s(root(v)) - \log s(v)) + O(\log n)\\),因此我們得到均攤 \\(O(\log N) \\)的時間複雜度。 + +需要更清楚的解釋可以參考 ([維基百科](https://en.wikipedia.org/wiki/Link/cut_tree)) + +有了 ``access()`` 的時間複雜度後,發現剩下的操作都是基於 ``access()`` 以及 ``splay()``,因此各個 LCT 操作的時間複雜度皆是\\( O(\log N) \\) + +|操作|時間複雜度| +|-----|--------| +|``access()``|\\( O(\log N) \\)| +|``make_root()``|\\( O(\log N) \\)| +|``link()``|\\( O(\log N) \\)| +|``cut()``|\\( O(\log N) \\)| +|``find_root()``|\\( O(\log N) \\)| + +### 最終模板 + +
Template Code + +```cpp +struct LCT +{ + #define cur node[x] + #define lc child[0] + #define rc child[1] + struct splay_node + { + int child[2], pa; + bool rev; + splay_node() : pa(0), rev(false), child({0, 0}) {} + }; + std::vector node; + LCT(int _size) { node.resize(_size + 1); } + bool isroot(int x) + { + return node[cur.pa].lc != x && node[cur.pa].rc != x; + } + void down(int x) + { + if (cur.rev) + { + swap(cur.lc, cur.rc); + if (cur.lc) + node[cur.lc].rev ^= 1; + if (cur.rc) + node[cur.rc].rev ^= 1; + cur.rev = 0; + } + } + void push_down(int x) + { + if (!isroot(x)) + push_down(cur.pa); + down(x); + } + void up(int x) {} + void rotate(int x) + { + int y = cur.pa, z = node[y].pa, d = (node[y].rc == x); + cur.pa = z; + if (!isroot(y)) + node[z].child[node[z].rc == y] = x; + node[y].child[d] = cur.child[d ^ 1]; + node[node[y].child[d]].pa = y; + node[y].pa = x, cur.child[d ^ 1] = y; + up(y); + up(x); + } + void splay(int x) + { + push_down(x); + while (!isroot(x)) + { + int y = cur.pa; + if (!isroot(y)) + { + int z = node[y].pa; + if ((node[z].lc == y) ^ (node[y].lc == x)) + rotate(x); + else + rotate(y); + } + rotate(x); + } + } + int access(int x) + { + int last = 0; + for (; x; last = x, x = cur.pa) + { + splay(x); + cur.rc = last; + up(x); + } + return last; + } + void make_root(int x) + { + access(x); + splay(x); + cur.rev ^= 1; + } + void link(int x, int y) + { + make_root(x); + cur.pa = y; + } + void cut(int x, int y) + { + make_root(x); + access(y); + splay(y); + node[y].lc = 0; + cur.pa = 0; + } + void cut(int x) + { + access(x); + splay(x); + node[cur.lc].pa = 0; + cur.lc = 0; + } + int find_root(int x) + { + int res = access(x); + while (node[res].lc) + res = node[res].lc; + splay(res); + return res; + } + #undef cur + #undef lc + #undef rc +}; +``` + +
+ +## LCT 用途 + +### Template problem + +> [DYNACON1](https://www.spoj.com/problems/DYNACON1/) +> 有一棵無根樹總共有 \\(N\\) 個節點,一開始沒有邊,題目是對於樹進行 \\(Q\\) 筆,操作總共有三種: +> +> 1. 連接兩個原本沒有連結的節點 +> 2. 斷開已經有聯結的兩個節點 +> 3. 詢問兩個節點中是否有一條 path +> +> - \\( N, Q \leq 10^5 \\) + +解題思路: +可以把題目想像成一個森林,每個節點都是獨立的一棵樹,因此符合 LCT 是一個森林的性質。 +第一個操作的可以用 LCT 的 ``link()`` 來達成。 +第二個的操作可以用 LCT 的 ``cut()`` 來達成。所以只要能判斷兩個節點是否相連就可以回答問題了。 +要判斷兩個節點是否相連其實就等同於兩個節點是否在同一棵樹上,所以我們只要看兩個節點的根是否一樣就可以達成。 + +時間複雜度分析: +對於每一個操作都可以在\\( O(\log N) \\)的時間完成,因此總複雜度就是\\( O(Q \log N) \\) + +
Solution Code + +在 ``cut()`` 之前要先判斷有沒有邊存在,但在這一題他保證邊一定存在,因此可以大膽的給他 ``cut()`` 下去 + +```cpp +#include +using namespace std; +// LCT template +int main() +{ + ios::sync_with_stdio(false); + cin.tie(0); + int n, q; + cin >> n >> q; + LCT lct(n); + while (q--) + { + string str; + int u, v; + cin >> str >> u >> v; + if (str[0] == 'a') + lct.link(u, v); + else if (str[0] == 'r') + lct.cut(u, v); + else if (lct.find_root(u) == lct.find_root(v)) + cout << "YES\n"; + else + cout << "NO\n"; + } + return 0; +} +``` + +
+ +### Maintaining edge weight on LCT + +> [SPOJ QTREE](https://www.spoj.com/problems/QTREE/) +> 給你一棵\\( N \\)個節點的樹,每個邊有邊權\\( w \\),然後會有\\( Q \\)筆操作。操作有兩種: +> +> 1. 改動其中一條邊的邊權 +> 2. 詢問節點\\( u \\)到節點\\( v \\)路徑上權重最大的邊 +> +> - \\( N, Q \leq 10^4 \\) + +解題思路:這題可以使用輕重鏈剖分做,但這裡提供 LCT 的做法。(同時也可以比較一下這兩種作法的時間複雜度分別會是多少) +在 LCT 中我們沒辦法維護邊權,因此我們可以利用**拆邊**的技巧,也就是將將一條邊變成一個點加上兩個邊,而那個點的權重變成原本邊的權重,可以參考下圖: + + +解決邊權的問題後,但在建樹之前要先維護好節點的資訊 +對於每個節點,需要維護: + +1. 點權 (程式碼中利用 ``val`` 表示) +2. Splay Tree 點權的最大值 (程式碼中利用 ``mx`` 表示) + +因為需要維護最大值,因此需要改動 ``up()`` 函式,當前節點的最大值就是小孩的最大值跟自己的點權取 max + +```cpp +void up(int x) +{ + cur.mx = max(max(node[cur.lc].mx, node[cur.rc].mx), cur.val); +} +``` + +對於題目的操作可以用下面方式達成: + +1. 改動其中一條邊的邊權 +直接更改那條邊拆點後的點權即可 +2. 詢問路徑上最大的邊 +與 ``cut()`` 操作相似,先讓 ``u`` 節點變成根,再把 ``v`` 節點旋轉到根,``v`` 節點的值就是答案 + +時間複雜度分析: +建樹時需要建立\\( 2N - 2 \\)條邊,建邊時間複雜度為\\( O(\log N) \\),因此建樹時時間複雜度是\\( O(N\log N) \\) +對於每一個操作都可以在\\( O(\log N) \\)的時間完成,因此\\( Q \\)筆操作時間複雜度就是\\( O(Q \log N) \\) +總時間複雜度為\\( O(N\log N + Q \log N) \\) + +
Solution Code + +```cpp +#include +using namespace std; +struct LCT +{ + void up(int x) + { + cur.mx = max(max(node[cur.lc].mx, node[cur.rc].mx), cur.val); + } + /* other LCT template */ +}; + +void solve() +{ + int n, u, v, w; + cin >> n; + LCT lct(2 * n); + for (int i = 1; i < n; i++) // n + i represent ith edge + { + cin >> u >> v >> w; + lct.node[n + i].val = w; + lct.link(u, n + i); + lct.link(n + i, v); + } + string op; + while (true) + { + cin >> op; + if (op == "DONE") + break; + else if (op == "CHANGE") + { + cin >> u >> w; + u += n; + lct.access(u); + lct.splay(u); + lct.node[u].val = w; + } + else + { + cin >> u >> v; + lct.make_root(u); + lct.access(v); + lct.splay(v); + cout << lct.node[v].mx << '\n'; + } + } +} + +int main() +{ + ios::sync_with_stdio(false); + cin.tie(0); + + int t; + cin >> t; + while (t--) + { + solve(); + } + return 0; +} +``` + +
+ +### ``access()`` 的其他應用 + +> [CF 117E - Tree or not Tree](https://codeforces.com/problemset/problem/117/E) +> 給你 \\(N\\) 個車站,一個車站可以連接多個車站,但每次只能往其中一個車站,總共有 \\(N - 1\\) 條單向權重為 \\(d\\) 的鐵路,並且 \\(1\\) 號車站為根節點。 +> 有 \\(M\\) 台火車,每個火車會在 \\(t_i\\) 的時間進入 \\(1\\) 號車站,並且目標車站為 \\(s_i\\)。 +> 在每個時間,你可以對火車站的開關操作,讓火車站前往不同的車站,如果火車無法到達目標車站,則火車會爆炸。 +> 題目要問最晚的爆炸時間,以及最少的操作次數。 +> +> - \\( N, M \leq 10^5 \\) +> - \\( 1 \leq d ,s_i, t_i \leq 10^9\\) + +雖然沒有明確的 Link 與 Cut 的需要,但仔細思考看看發現這題的火車站所前往的方向,就很像 LCT 中的 preferred edge,而火車前往的路徑就是 LCT 中的 preferred path,因此可以用 LCT 試試看。把每個 LCT 節點想成一個火車站,紀錄當前距離 \\(1\\) 號車站的距離,並且記錄當前最晚被操作的時間點。在 ``access()`` 中每一次的 iteration 都代表著一次操作,可以根據最晚操作的時間點,算出一個區間 \\\((L, R\]\\),代表開關只要在這個時間點中被更改方向,火車就可以前往正確的路徑。把這些區間放進一個 priority_queue 中維護,並且每次挑選最接近當前時間的開關進行操作,如果當前時間超過了當前最早需要的操作,代表火車會爆炸。 + +
Solution Code + +實作細節: + +1. 因為節點深度的資訊在這題中不重要,因此不需要維護,這題要維護的是火車站的最後使用到的時間,因此懶惰標記改成紀錄時間。 +2. 在 ``splay()`` 時,需要從當前輔助樹的根節點將懶惰標記下推,否則向上旋轉時,會出現懶惰標記跑掉的錯誤。 +3. 在 ``access()`` 時,需要紀錄區間 \\\((L, R\]\\),並把它記錄起來。 + +```cpp +#include +using namespace std; +typedef long long ll; +stack st; +vector>> G; +vector> op; +struct LCT +{ + struct splay_node + { + int child[2], parent; + ll time, tag, dist; + // Time means the last operation time. + // Dist is distance between 1 and current node. + splay_node() : parent(0), child(), time(0), tag(0), dist(0) {} + }; + vector node; + LCT(int _size) { node.resize(_size + 1); } + bool isroot(int x) + { + return node[cur.parent].lc != x && node[cur.parent].rc != x; + } + void down(int x) + { + if (cur.tag) + { + cur.time = cur.tag; + if (cur.lc) + node[cur.lc].tag = cur.tag; + if (cur.rc) + node[cur.rc].tag = cur.tag; + cur.tag = 0; + } + } + void rotate(int x) + { + int y = cur.parent, z = node[y].parent, d = (node[y].rc == x); + cur.parent = z; + if (!isroot(y)) + node[z].child[node[z].rc == y] = x; + node[y].child[d] = cur.child[d ^ 1]; + node[node[y].child[d]].parent = y; + node[y].parent = x, cur.child[d ^ 1] = y; + } + void splay(int x) + { /* Very important: Need to push down from root */ + int u = x; + st.push(u); + while (!isroot(u)) + st.push(u = node[u].parent); + while (st.size()) + { + u = st.top(); + st.pop(); + down(u); + } + + while (!isroot(x)) + { + int y = cur.parent; + if (!isroot(y)) + { + int z = node[y].parent; + if ((node[z].lc == y) ^ (node[y].lc == x)) + rotate(x); + else + rotate(y); + } + rotate(x); + } + } + int access(int x, int t) + { + int last = 0, ori = x; + for (; x; last = x, x = cur.parent) + { + splay(x); + if (last) + { + cur.rc = last; + a.emplace_back(cur.time + cur.dist, t + cur.dist); + } + } + splay(ori); + node[node[ori].lc].tag = t; + return last; + } + void dfs(int x, int pa = 0) + { /* Use dfs to build tree. */ + cur.parent = pa; + cur.time = -cur.dist; + for (auto [v, w] : G[x]) + { + if (v == pa) + continue; + cur.rc = v; + node[v].dist = cur.dist + w; + dfs(v, x); + } + } +}; + +int main() +{ + ios::sync_with_stdio(false); + cin.tie(0); + + int n, m; + cin >> n >> m; + LCT lct(n); + G.resize(n + 1); + for (int i = 1; i < n; i++) + { + int u, v, w; + cin >> u >> v >> w; + G[u].emplace_back(v, w); + G[v].emplace_back(u, w); + } + lct.dfs(1); + + for (int i = 1; i <= m; ++i) + { + int s, t; + cin >> s >> t; + lct.access(s, t); + } + sort(op.begin(), op.end()); + priority_queue, greater> q; + ll last = 0; + int idx = 0; + while (!q.empty() || idx != op.size()) + { + if (q.empty()) + last = op[idx].first; + while (idx != op.size() && op[idx].first == last) + q.push(op[idx++].second); + if (last == q.top()) + { + int ans = 0; + for (auto [start, end] : op) + if (end < last) + ans++; + cout << last << ' ' << ans << '\n'; + return 0; + } + q.pop(), last++; + } + cout << -1 << ' ' << op.size() << '\n'; + + return 0; +} +``` + +時間複雜度分析: +時間複雜度被操作所執行的次數所決定,由於 ``access()`` 操作有均攤 \\(O (\log N)\\) 的時間複雜度,因此操作最多執行的次數為 \\(O (M \log N)\\)。最終用 priority_queue 維護這些操作,因此最終的時間複雜度為 \\(O (N \log N + M \log^2 N)\\) + +
+ +### 總結 + +- LCT 可以判斷連通性 +- LCT 可以做路徑維護,因此可以維護路徑資訊 +- LCT 可以動態切邊,因此當看到 **cut** 關鍵字的時候,或許可以想想看 LCT 可不可以做,因為大部分輕重鏈剖分可以做的題目,LCT 也都可以做 +- LCT 的 ``access()`` 操作可以在 \\(O (\log N)\\) 的時間複雜度完成,因此可以利用這個性質去修改 ``access()``,讓操作有更好的時間複雜度 + +## Exercise + +模板題: +[DYNALCA - Dynamic LCA](https://www.spoj.com/problems/DYNALCA/) +
Solution Code + +詢問 lca 時,先 ``access()`` 其中一個節點,再 ``access()`` 另一個節點,即可找到 lca + +```cpp +#include +using namespace std; +// LCT template +int main() +{ + ios::sync_with_stdio(false); + cin.tie(0); + + int n, m, u, v; + cin >> n >> m; + LCT lct(n); + while (m--) + { + string query; + cin >> query; + if (query == "link") + { + cin >> u >> v; + lct.link(u, v); + } + else if (query == "cut") + { + cin >> u; + lct.cut(u); + } + else if (query == "lca") + { + cin >> u >> v; + lct.access(u); + cout << lct.access(v) << '\n'; + } + } + + return 0; +} +``` + +
+ +SPOJ-QTREE 系列題目: + +- [QTREE](https://www.spoj.com/problems/QTREE/) +- [QTREE2](https://www.spoj.com/problems/QTREE2/) +- [QTREE3](https://www.spoj.com/problems/QTREE3/) +- [QTREE4](https://www.spoj.com/problems/QTREE4/) +- [QTREE5](https://www.spoj.com/problems/QTREE5/) +- [QTREE6](https://www.spoj.com/problems/QTREE6/) +- [QTREE7](https://www.spoj.com/problems/QTREE7/) + +QTREE 系列題目可以參考這篇文章:[【Qtree】Query on a tree 系列 LCT 解法](https://blog.csdn.net/thy_asdf/article/details/50768620) + +CF 相關題目: + +- [CF 13E - Holes](https://codeforces.com/contest/13/problem/E) +- [CF 117E - Tree or not Tree](https://codeforces.com/problemset/problem/117/E) +- [CF 342E - Xenia and Tree](https://codeforces.com/problemset/problem/342/E) +- [CF 1172E - Nauuo and ODT](https://codeforces.com/contest/1172/problem/E) +- [CF 1344E - Train Tracks](https://codeforces.com/problemset/problem/1344/E) + +## 參考資料 + +- [Link/cut tree - Wikipedia](https://en.wikipedia.org/wiki/Link/cut_tree) +- [Link Cut Tree - OI Wiki](https://oi-wiki.org/ds/lct/) +- [日月卦長的模板庫: [ link-cut tree ] 動態樹教學+模板]() +- [Splay tree tutorial - Codeforces](https://codeforces.com/blog/entry/79524) +- [Link-cut tree tutorial - Codeforces](https://codeforces.com/blog/entry/80383) +- [【Qtree】Query on a tree 系列 LCT 解法](https://blog.csdn.net/thy_asdf/article/details/50768620)