1. 程式人生 > 其它 >Link Cut Tree

Link Cut Tree

Link/Cut Tree 又稱 Link Cut Tree,簡稱 LCT,是一類用來解決動態樹問題的資料結構。

Splay 是 LCT 的基礎。但是 LCT 在使用 Splay 的時候,還將其進行了一些~~魔改~~擴充套件。

LCT 與 Splay 一樣,包含了一(dà)些(liàng)函式來維持 LCT 的運轉。

# 問題引入

假設我們需要維護一棵樹,使其支援如下操作:

- 修改兩點間路徑權值。
- 查詢兩點間路徑權值和。
- 修改某點子樹權值。
- 查詢某點子樹權值和。

唔,看上去是一道樹剖模版題。

那麼我們再加一個操作:

- 斷開並連線一些邊,保證仍是一棵樹。

要求線上求出上面的答案。

<p>——這就需要動態樹問題的解決方法:<ruby>リンク<rp>(</rp><rt>Link</rt><rp>)</rp></ruby><ruby>/<rp>(</rp><rt>/</rt><rp>)</rp></ruby><ruby>カット<rp>(</rp><rt>Cut</rt><rp>)</rp></ruby><ruby>ツリー<rp>(</rp><rt>Tree</rt><rp>)</rp></ruby>!</p>

## 什麼是動態樹問題

維護一個森林,支援刪除某條邊,加入某條邊,並保證加邊,刪邊之後仍是森林。動態樹問題要求我們要維護這個森林的一些資訊。

一般的操作有詢問兩點連通性,詢問兩點路徑權值和,連線兩點或切斷某條邊、修改資訊等。

## 路徑……ん……樹鏈剖分?

既然需要詢問路徑上的問題,那麼就可以想到使用樹剖來幫助我們解決這樣的問題。

但是……
樹剖對LCT的適用性怎麼樣?

### 從 LCT 的角度回顧一下樹鏈剖分

首先我們對整棵樹按子樹大小進行了剖分,並重新按照dfs序標了號。

我們發現重新標號之後,在樹上形成了一些以鏈為單位的連續區間,並且可以用線段樹進行區間操作。

### 轉向動態樹問題

我們發現我們剛剛講的樹剖是以子樹大小作為劃分條件。那我們能不能重定義一種剖分,使它更適應我們的動態樹問題呢?

考慮動態樹問題需要什麼鏈。

由於動態維護一個森林,顯然我們希望這個鏈是我們指定的鏈,以便利用來求解。

這就需要……

### 實鏈剖分!

對於一個點連向它所有兒子的邊,我們自己選擇一條邊進行剖分。
我們稱被選擇的邊為實邊,其他邊則為虛邊。

對於實邊,我們稱它所連線的兒子為實兒子。
對於一條由實邊組成的鏈,我們同樣稱之為實鏈。

請記住我們選擇實鏈剖分的最重要的原因:它是我們選擇的,靈活且可變。
正是它的這種靈活可變性,我們便可以採用 Splay 來維護這些實鏈。

# LCT!

我們可以簡單的把 LCT 理解成用一些 Splay 來維護動態的樹鏈剖分,以期實現動態樹上的區間操作。對於每條實鏈,我們建一個 Splay 來維護整個鏈區間的資訊。

接下來,我們來學習 LCT 的具體結構。

## 輔助樹

鑑於我們維護的是一個森林,那麼我們就需要一些東西將所有的樹連成一個整體來維護。

於是我們就推出了——輔助樹!

我們可以認為,一些Splay構成了一棵輔助樹,而一些輔助樹構成了LCT,其維護的是整個森林。

1. 輔助樹由多棵 Splay 組成,每棵 Splay 維護原樹中的一條路徑,且中序遍歷這棵 Splay 得到的點序列,從前到後對應原樹“從上到下”的一條路徑。
2. 原樹每個節點與輔助樹的 Splay 節點一一對應。
3. 輔助樹的各棵 Splay 之間並不是獨立的。每棵 Splay 的根節點的父親節點本應是空,但在 LCT 中每棵 Splay 的根節點的父親節點指向原樹中 **這條鏈** 的父親節點(即鏈最頂端的點的父親節點)。這類父親連結與通常 Splay 的父親連結區別在於兒子認父親,而父親不認兒子,對應原樹的一條 **虛邊**。因此,每個連通塊恰好有一個點的父親節點為空。
4. 由於輔助樹的以上性質,我們維護任何操作都不需要維護原樹,輔助樹可以在任何情況下拿出一個唯一的原樹,我們只需要維護輔助樹即可。

### 考慮原樹和輔助樹的結構關係

- 原樹中的實鏈 : 在輔助樹中節點都在一棵 Splay 中。
- 原樹中的虛鏈 : 在輔助樹中,子節點所在 Splay 的 Father 指向父節點,但是父節點的兩個兒子都不指向子節點。
- 注意:原樹的根不等於輔助樹的根。
- 原樹的 Father 指向不等於輔助樹的 Father 指向。
- 輔助樹是可以在滿足輔助樹、Splay 的性質下任意換根的。
- 虛實鏈變換可以輕鬆在輔助樹上完成,這也就是實現了動態維護樹鏈剖分。


## 實現

這裡我們以[洛谷板子題](https://www.luogu.com.cn/problem/P3690)為例。

### 接下來會用到的變數宣告

- `s[N][2]` 左右兒子
- `fa[N]` 節點的父親指向
- `sum[N]` 路徑權值和
- `val[N]` 點權
- `rev[N]` 翻轉標記
- `sz[N]` 輔助樹上子樹大小
- 其他可能用到的變數

其中,`s[2]`,`fa`,`val`,`rev`和`sum`我們定義為了一個結構體,就像這樣:
``` cpp
struct Node
{
int s[2], fa, val;
int sum, rev;
}tr[N];
```

### 接下來會用到的函式宣告

#### 一般資料結構函式

- `pushup(x)`
- `pushdown(x)`

#### Splay系函式

- `rotate(x)` 將 $x$ 向上旋轉。
- `splay(x)` 通過與`rotate`聯動來實現將 $x$ 旋轉至**當前Splay的根**。

#### LCT獨有的新函式

- `access(x)` 把從根到 $x$ 的所有點放在一條實鏈裡,使根到 $x$ 成為一條實路徑,並且在同一棵 Splay 裡。**只有此操作是必須實現的,其他操作視題目而實現。**
- `isroot(x)` 判斷 $x$ 是否是所在樹的根。
- `update(x)` 在 `access` 操作之後,遞迴地從上到下 `pushdown` 更新資訊。
- `makeroot(x)` 使 $x$ 點成為其所在樹的根。
- `link(x, y)` 在 $x, y$ 兩點間連一條邊。
- `cut(x, y)` 把 $x, y$ 兩點間邊刪掉。
- `findroot(x)` 找到 $x$ 所在樹的根節點編號。
- `fix(x, v)` 修改 $x$ 的點權為 $v$。
- `split(x, y)` 提取出 $x, y$ 間的路徑,方便做區間操作。

## 函式講解

### `pushup()` 與 `pushdown()`

這兩個函式具體根據你所需要的維護的資訊來寫。
在本題內就是這樣的:

``` cpp
void pushup(int x)
{
tr[x].sum = tr[tr[x].s[0]].sum ^ tr[x].val ^ tr[tr[x].s[1]].sum;
}
```

``` cpp
void pushdown(int x)
{
if(tr[x].rev)
{
pushrev(tr[x].s[0]);
pushrev(tr[x].s[1]);
tr[x].rev = 0;
}
}
```

同時我們因為需要進行翻轉操作,於是就會對翻轉標記進行一次pushdown。我們使用了函式來進行操作,但是這個函式可有可無:既可以放進其他函式的程式碼內,也可以單獨拿出來。我們這裡單獨拿了出來,放在了pushup和pushdown的前面。函式如下:

``` cpp
void pushrev(int x)
{
swap(tr[x].s[0], tr[x].s[1]);
tr[x].rev ^= 1;
}
```

### `splay()` 與 `rotate()`

這兩個是 Splay 系的函式。

有點不一樣了呢。

```cpp
void rotate(int x)
{
int y = tr[x].fa, z = tr[y].fa;
int k = tr[y].s[1] == x;
if(!isroot(y)) tr[z].s[tr[z].s[1] == y] = x;
tr[x].fa = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].fa = y;
tr[x].s[k ^ 1] = y, tr[y].fa = x;
pushup(y), pushup(x);
}
```
``` cpp
void splay(int x)
{
int top = 0, r = x;
stk[++top] = r;
while(!isroot(r)) stk[++top] = r = tr[r].fa;
while(top) pushdown(stk[top--]);
while(!isroot(x))
{
int y = tr[x].fa, z = tr[y].fa;
if(!isroot(y))
if((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
}
```

相關解釋見[Splay](/OI/splay)。

### `isroot()`

第一個 LCT 系函式。

``` cpp
bool isroot(int x)
{
return (tr[tr[x].fa].s[0] != x) && (tr[tr[x].fa].s[1] != x);
}
```

在前面我們已經說過,LCT 具有 如果一個兒子不是實兒子,他的父親找不到它的性質。
所以當一個點既不是它父親的左兒子,又不是它父親的右兒子,它就是當前 Splay 的根。

### `access()`

另一個 LCT 系函式。

``` cpp
void access(int x)
{
int z = x;
for(int y = 0; x; y = x, x = tr[x].fa)
{
splay(x);
tr[x].s[1] = y, pushup(x);
}
splay(z);
}
```

access 是 LCT 的核心操作,試想我們像求解一條路徑,而這條路徑恰好就是我們當前的一棵 Splay,直接呼叫其資訊即可。

`access()` 其實很容易,只有如下四步操作:

1. 把當前節點轉到根。
2. 把兒子換成之前的節點。
3. 更新當前點的資訊。
4. 把當前點換成當前點的父親,繼續操作。

### `makeroot()`

``` cpp
void makeroot(int x)
{
access(x);
pushrev(x);
}
```

### `link()`

link兩個點其實很簡單,先`makeroot(x)`,然後把 $x$ 的父親指向 $y$ 即可。顯然,這個操作肯定不能發生在同一棵樹內,所以記得先判一下。

``` cpp
void link(int x, int y)
{
makeroot(x);
if(findroot(y) != x) tr[x].fa = y;
}
```

### `cut()`

`cut` 有兩種情況,保證合法和不一定保證合法。(廢話)

如果保證合法,直接 `split(x, y)`,這時候 $y$ 是根,$x$ 一定是它的兒子,雙向斷開即可。

如果是不保證合法,我們需要判斷一下是否有邊。
想要刪邊,必須要滿足如下三個條件:

1. $x,y$ 連通。
2. $x$ 是 $y$ 的父親。
3. $y$ 沒有左兒子。

總結一下,上面三句話的意思就一個:$x,y$ 之間聯通,且其間沒有其他節點。

``` cpp
void cut(int x, int y)
{
makeroot(x);
if((findroot(y) == x) && (tr[y].fa == x) && (!tr[y].s[0]))
{
tr[x].s[1] = tr[y].fa = 0;
pushup(x);
}
}
```

### `split()`

`split` 操作意義很簡單,就是拿出一棵splay,維護的是 $x$ 到 $y$ 的路徑。
先 `makeroot(x)`,然後 `access(y)`。如果要 $y$ 做根,再 `splay(y)`。
另外split這三個操作直接可以把需要的路徑拿出到 $y$ 的子樹上,那不是隨便幹嘛咯。
這裡不需要 $y$ 做根,所以就沒有寫 `splay(y)`。

``` cpp
void split(int x, int y)
{
makeroot(x);
access(y);
}
```

### `findroot()`

`findroot()` 其實就是找到當前輔助樹的根。在 `access(y)` 後,再 `splay(y)`。這樣根就是樹裡最小的那個,一直往左兒子走,沿途 `pushdown` 即可。
一直走到沒有左兒子,非常簡單。
注意,每次查詢之後需要把查詢到的答案對應的結點 `splay` 上去以保證複雜度。

``` cpp
int findroot(int x)
{
access(x);
while(tr[x].s[0]) pushdown(x), x = tr[x].s[0];
splay(x);
return x;
}
```

### 一些提醒

- 乾點啥前一定要想一想需不需要 `pushup` 或者 `pushdown`,LCT由於特別靈活的原因,少 `pushdown` 或者 `pushup` 一次就可能把修改改到不該改的點上!
- LCT的 `rotate` 和 splay 的不太一樣,`if(z)` 一定要放在前面。
- LCT的 `splay` 操作就是旋轉到根,沒有旋轉到誰兒子的操作,因為不需要。

## 全部加起來

``` cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;

int n, m;
struct Node
{
int s[2], fa, val;
int sum, rev;
}tr[N];
int stk[N];

void pushrev(int x)
{
swap(tr[x].s[0], tr[x].s[1]);
tr[x].rev ^= 1;
}

void pushup(int x)
{
tr[x].sum = tr[tr[x].s[0]].sum ^ tr[x].val ^ tr[tr[x].s[1]].sum;
}

void pushdown(int x)
{
if(tr[x].rev)
{
pushrev(tr[x].s[0]);
pushrev(tr[x].s[1]);
tr[x].rev = 0;
}
}

bool isroot(int x)
{
return (tr[tr[x].fa].s[0] != x) && (tr[tr[x].fa].s[1] != x);
}

void rotate(int x)
{
int y = tr[x].fa, z = tr[y].fa;
int k = (tr[y].s[1] == x);
if(!isroot(y)) tr[z].s[tr[z].s[1] == y] = x;
tr[x].fa = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].fa = y;
tr[x].s[k ^ 1] = y, tr[y].fa = x;
pushup(y), pushup(x);
}

void splay(int x)
{
int top = 0, r = x;
stk[++top] = r;
while(!isroot(r)) stk[++top] = r = tr[r].fa;
while(top) pushdown(stk[top--]);
while(!isroot(x))
{
int y = tr[x].fa, z = tr[y].fa;
if(!isroot(y))
if((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
}

void access(int x) // 建立一條從根到x的路徑,同時將x變成splay的根節點
{
int z = x;
for(int y = 0; x; y = x, x = tr[x].fa)
{
splay(x);
tr[x].s[1] = y, pushup(x);
}
splay(z);
}

void makeroot(int x) // 將x變成原樹的根節點
{
access(x);
pushrev(x);
}

int findroot(int x) // 找到x所在原樹的根節點, 再將原樹的根節點旋轉到splay的根節點
{
access(x);
while(tr[x].s[0]) pushdown(x), x = tr[x].s[0];
splay(x);
return x;
}

void split(int x, int y) // 給x和y之間的路徑建立一個splay,其根節點是y
{
makeroot(x);
access(y);
}

void link(int x, int y) // 如果x和y不連通,則加入一條x和y之間的邊
{
makeroot(x);
if(findroot(y) != x) tr[x].fa = y;
}

void cut(int x, int y) // 如果x和y之間存在邊,則刪除該邊
{
makeroot(x);
if((findroot(y) == x) && (tr[y].fa == x) && (!tr[y].s[0]))
{
tr[x].s[1] = tr[y].fa = 0;
pushup(x);
}
}

int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &tr[i].val);
while(m--)
{
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if(t == 0)
{
split(x, y);
printf("%d\n", tr[y].sum);
}
else if(t == 1) link(x, y);
else if(t == 2) cut(x, y);
else
{
splay(x);
tr[x].val = y;
pushup(x);
}
}
return 0;
}
```

{% note 封裝好了的版本 %}

``` cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;

int n, m;
struct Link_Cut_Tree
{
int s[N][2], fa[N], val[N];
int sum[N], rev[N];

int stk[N];

void pushrev(int x)
{
swap(s[x][0], s[x][1]);
rev[x] ^= 1;
}

void pushup(int x)
{
sum[x] = sum[s[x][0]] ^ val[x] ^ sum[s[x][1]];
}

void pushdown(int x)
{
if(rev[x])
{
pushrev(s[x][0]);
pushrev(s[x][1]);
rev[x] = 0;
}
}

bool isroot(int x)
{
return (s[fa[x]][0] != x) && (s[fa[x]][1] != x);
}

void rotate(int x)
{
int y = fa[x], z = fa[y];
int k = (s[y][1] == x);
if(!isroot(y))s[z][s[z][1] == y] = x;
fa[x] = z;
s[y][k] = s[x][k ^ 1], fa[s[x][k ^ 1]] = y;
s[x][k ^ 1] = y, fa[y] = x;
pushup(y), pushup(x);
}

void splay(int x)
{
int top = 0, r = x;
stk[++top] = r;
while(!isroot(r)) stk[++top] = r = fa[r];
while(top) pushdown(stk[top--]);
while(!isroot(x))
{
int y = fa[x], z = fa[y];
if(!isroot(y))
if((s[y][1] == x) ^ (s[z][1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
}

void access(int x)
{
int z = x;
for(int y = 0; x; y = x, x = fa[x])
{
splay(x);
s[x][1] = y;
pushup(x);
}
splay(z);
}

void makeroot(int x)
{
access(x);
pushrev(x);
}

int findroot(int x)
{
access(x);
while(s[x][0]) pushdown(x), x = s[x][0];
splay(x);
return x;
}

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

void link(int x, int y)
{
makeroot(x);
if(findroot(y) != x) fa[x] = y;
}

void cut(int x, int y)
{
makeroot(x);
if((findroot(y) == x) && (fa[y] == x) && (!s[y][0]))
{
s[x][1] = fa[y] = 0;
pushup(x);
}
}
}tr;

int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &tr.val[i]);
while(m--)
{
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if(t == 0)
{
tr.split(x, y);
printf("%d\n", tr.sum[y]);
}
else if(t == 1) tr.link(x, y);
else if(t == 2) tr.cut(x, y);
else
{
tr.splay(x);
tr.val[x] = y;
tr.pushup(x);
}
}

return 0;
}
```

{% endnote %}