---
tags: 成大高階競技程式設計 2021
---
# 2021 Week12 - Graph (BCC, SCC, LCA)
註::bulb: 代表相對少見、較進階或與主題關聯性較低的內容,讀者可自行斟酌觀看。
---
### 無向圖
* 連通圖(connected graph)
* 任意二點 $u, v$,都有從 $u$ 到 $v$ 的路徑的圖
* 連通分量(connected component)
* 極大的(maximal)連通子圖(subgraph)
<hr class="thin">
* 雙連通圖(biconnected graph)
* 移除任何一個點,圖還是連通的圖
* 雙連通分量(biconnected component)
* 極大的雙連通子圖
<hr class="dotted">
### 有向圖
* 強連通圖(strongly connected graph)
* 任意二點 $u, v$ 都有從 $u$ 到 $v$ 的路徑的圖(當然也有從 $v$ 到 $u$ 的路徑)
* 強連通分量(strongly connected component)
* 極大的強連通子圖
<hr class="dotted">
### DFS tree
一條邊 $(u,v)$ 的種類是
* tree edge
* DFS 時走過的邊
* back edge
* $v$ 是 $u$ 的祖先(且 $(u,v)$ 不是 tree edge)
* forward edge
* $v$ 是 $u$ 的子孫(且 $(u,v)$ 不是 tree edge)
* cross edge
* 其他
```graphviz
digraph {
A [label="A"];
B [label="B"];
C [label="C"];
D [label="D"];
E [label="E"];
F [label="F"];
G [label="G"];
H [label="H"];
A -> B;
B -> C;
C -> D;
D :e -> B [style=dashed, label="back"];
A -> E;
E -> F;
F -> G;
A :e -> G [style=tapered, label="forward"];
A :se -> H;
H :w -> F :e [style=dotted, label="cross"];
}
```
> **無向圖的邊只視為 tree edge 或 back edge**
---
# 關節點(AP, articulation point, cut vertex)
> 這裡只考慮無向圖
> articulation 也是連接的意思。
**一個點如果被移除,連通分量會增加的話,就稱為關節點。**
```graphviz
graph {
rankdir=LR;
A [label="A"];
B [label="B", style=filled, fillcolor="red"];
C [label="C"];
D [label="D", style=filled, fillcolor="red"];
E [label="E"];
F [label="F"];
A -- B;
B -- C;
C -- D;
D :ne -- B :nw;
D -- E;
E -- F;
F :se -- D :sw;
}
```
<hr class="thin">
在 DFS tree 上,一個節點 $u$ 與其子節點 $v$,
如果 $v$ 存在另一條不通過 $u$ 的路徑,可以到達 $u$ 的祖先,
這樣移除 $u$ 之後,子樹 $v$ [^subtree] 也不會變為新的連通分量;
反之,移除 $u$ 之後,子樹 $v$ 就會形成新的連通分量。
但有個特例是樹根,樹根沒有祖先,而因為無向圖沒有 cross edge,
不同子樹之間一定要通過樹根才能相連,所以樹根是不是關節點,
只需要看子樹數量是否大於 $1$。
> $v$ 要不通過 $u$ 而到達 $u$ 的祖先,就只能先往 $v$ 的子孫走,
> 再經過 back edge;
> 另一種理解方式是,因為子樹 $v$ 本身是連通的,
> 所以 $v$ 或 $v$ 的子孫要有一個鄰居是 $u$ 的祖先,
>這樣 $u$ 被移除後,子樹 $v$ 與 $u$ 的祖先還是連通的。
定義 $dep(u)$ 為 $u$ 的深度。
定義 $low(v)$ 為 $v$ 先往子孫走,再通過不超過一次的 back edge,
所能達到的節點的最小深度;
換句話說,就是 $v$ 與 $v$ 的子孫節點的「鄰居」中,節點的最小深度。
```cpp!
void dfs(int u, int w = -1) {
int child{0};
low[u] = dep[u] = ++d;
for (auto& v : adj[u]) {
if (v == w) continue;
if (!dep[v]) { // tree edge
++child;
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] >= dep[u]) ap[u] = true;
} else low[u] = min(low[u], dep[v]); // back edge
}
if (w == -1) ap[u] = (child > 1); // root
--d;
}
```
> 為什麼不定義成可以通過好幾次 back edge 呢?
> 因為我們會考慮子樹 $v$ 所有的點,
> 如果通過 back edge 是到子樹中其他點的話,是沒有意義的;
> 如果通過 back edge 已經到了 $u$ 的祖先的話,那也足夠判斷了。
>
> > 而通過多個 back edge 可能出錯,如下圖的情況。
>
> ```graphviz
> digraph {
>
> A -> B;
> B -> C;
> C -> D;
> D :e -> B [style=dashed, label="back"];
> B :e -> A [style=dashed, label="back"];
> B -> E;
> E -> F;
> F -> G;
> E :ne -> A :se [style=dashed, label="back"];
> G :ne -> E :se [style=dashed, label="back"];
> }
> ```
>
> > 我們可能會以為 $C$ 可以通過 $D$ 到 $A$,
> > 但其實還是要通過 $B$,所以 $B$ 也還是關節點。
<hr class="dashed">
# 橋(bridge)
**一條邊如果被移除,連通分量會增加的話,就被稱為橋。**
```graphviz
graph {
rankdir=LR;
A [label="A"];
B [label="B"];
C [label="C"];
D [label="D"];
E [label="E"];
F [label="F"];
A -- B;
B -- C;
C -- D [color="red"];
C :ne -- A :nw;
D -- E;
E -- F;
F :se -- D :sw;
}
```
跟找關節點很像,如果 $low(v)\gt dep(u)$,
代表子樹 $v$ 沒有其他路徑,可以到達 $u$ 或 $u$ 的祖先,
移除 $(u,v)$ 的話,子樹 $v$ 就會形成新的連通分量。
```cpp!
void dfs(int u, int w = -1) {
low[u] = dep[u] = ++d;
for (auto& v : adj[u]) {
if (v == w) continue;
if (!dep[v]) { // tree edge
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dep[u]) bridge.emplace_back(u, v);
} else low[u] = min(low[u], dep[v]); // back edge
}
--d;
}
```
<hr class="dashed">
# 雙連通分量(BCC, biconnected component)
**移除任何一個點,子圖還是連通的,也就是說沒有關節點。**
那一個圖分成雙連通分量會長怎麼樣呢?關節點一樣可以存在某些分量當中,
但如果是在關節點移除後,會不連通的部分,就要在不同的分量中,
這樣關節點移除的話,同一個雙連通分量中,點之間才不會被分開。
![](https://upload.wikimedia.org/wikipedia/commons/6/66/Graph-Biconnected-Components.svg)[^wikiBCC]
一樣先把關節點找出來,遇到關節點時就把圖拆成不同的分量。
> 樹根的 `low[v] >= dep[u]` 一定成立,
> 等於說每棵子樹都會拆成一個雙連通分量。
> (所以不管樹根是不是關節點,都是正確的。)
> 如果 `dep[v] >= dep[u]`,代表它是 back edge $(v,u)$,所以就不要管它。
>
> > **$v$ 是 $u$ 的子孫**,$(u,v)$ 其實是 forward edge 的概念,
> > 只是在無向圖中,$(u,v)$ 與 $(v,u)$ 是同一條邊,視為 back edge。
```cpp!
class BCC {
int d;
vector<int> dep{}, low{};
vector<int> bccno{};
vector<vector<int>> bcc{};
stack<pair<int, int>> s{};
void dfs(const vector<vector<int>>& adj, int u, int w = -1) {
low[u] = dep[u] = ++d;
for (auto& v : adj[u]) {
if (v == w || dep[v] >= dep[u]) continue;
if (!dep[v]) { // tree edge
s.emplace(u, v);
dfs(adj, v, u);
low[u] = min(low[u], low[v]);
if (low[v] >= dep[u]) {
bcc.emplace_back();
while (true) {
auto [x, y]{s.top()};
s.pop();
if (bccno[x] != bcc.size()) bcc.back().push_back(x), bccno[x] = bcc.size();
if (bccno[y] != bcc.size()) bcc.back().push_back(y), bccno[y] = bcc.size();
if (x == u && y == v) break;
}
}
} else low[u] = min(low[u], dep[v]); // back edge
}
--d;
}
public:
vector<vector<int>> operator()(const vector<vector<int>>& adj) {
int n{adj.size()};
dep.assign(n, 0), low.resize(n), bccno.assign(n, 0);
for (int i{0}; i < n; ++i) if (!dep[i]) dfs(adj, i);
return move(bcc);
}
};
```
> 此演算法由 John Hopcroft & Robert Tarjan 提出,並無正式名稱。
---
# 強連通分量(SCC, strongly connected component)
```graphviz
digraph {
rankdir=LR;
node [label=""];
A, B, C, D, E [color=red];
F [color=green];
G, H, I, J [color=purple];
A -> B;
B -> C;
C -> A;
C -> D [dir=back];
D -> E [dir=back];
E -> C [dir=back];
F -> C [dir=back];
F -> E [dir=back];
F -> G;
E -> G;
G -> H;
G -> H [dir=back];
H -> I [dir=back];
J -> I;
G -> J;
J -> H;
}
```
### Tarjan's strongly connected components algorithm
> 跟找雙連通分量很像。
定義 $dfn(v)$ 為 $v$ 被拜訪的順序。
定義 $low(v)$ 為子樹 $v$ 與它們的強連通「鄰居」中,最早被拜訪的節點的順序。
不管 DFS 從誰開始,每個強連通分量總會有第一個被拜訪的點 $w$,使得 `low[w] == dfn[w]`;
除此之外,對於每個點 $v$,子樹 $v$ 總要有一個強連通鄰居比 $v$ 更早被拜訪,所以 `low[v] < dfn[v]`;
因此我們只需要找出 $w$,其他同個強連通分量中的點,一定是 $w$ 的子孫。
我們一樣用一個堆疊來記錄經過的點,當我們找到一個 $w$ (`low[w] == dfn[w]` 的節點)時,
堆疊中在 $w$ 之上的點就是一個強連通分量,我們就可以把這個強連通分量從堆疊中取出。
> 而當節點被拜訪完畢,卻還被留在堆疊中,代表 `low[v] < dfn[v]`,
> 也就是它可以到某個堆疊中更底下的點。
<hr class="thin">
強連通鄰居的話,就只能透過 back edge 或 cross edge 相連。
> **不是所有的 cross edge 都可以,要看彼此之間有沒有強連通。**
> 而所有的 back edge 一定可以,因為彼此之間一定是強連通的。
通過某個 cross edge $(v,u)$,如果 $u$ 還不在某個已知的強連通分量當中,
也就是說它還在堆疊中,這代表 $u$ 可以到 $lca(u,v)$,這也代表 $v$ 也可以到 $lca(u,v)$,
所以 $v$ 與 $lca(u,v)$ 也是強連通的(當然 $v$ 與 $u$ 就是強連通的)。
```graphviz
digraph {
A [label="A,1"];
B [label="B,2"];
C [label="C,3"];
D [label="D,4"];
E [label="E,5"];
F [label="F,6"];
G [label="G,7"];
A -> B;
B -> C;
C -> D;
D -> C [style=dashed];
C -> A [style=dashed];
A -> E;
E -> F;
F -> G;
G -> E [style=dashed];
F -> C :e [style=dotted, fillcolor="red", color="red"];
}
```
> 所以當我們找到某個強連通分量中,第一個被拜訪的點 $w$,並拜訪完後,
> 堆疊中在它之上的任意點 $v$,都可以通過多個點,最終到達 $w$,
> 又因為這些點都是 $w$ 的子孫,所以都與 $w$ 是強連通的。
>
> > 為什麼說這樣就已經是極大的強連通子圖了呢?
> > 因為還沒被拜訪的節點,代表這邊到它們沒有路徑;
> > 已經被拜訪的子孫節點,卻已經被從堆疊中拿出的,代表它們到這邊沒有路徑;
> > cross edge 到其他強連通分量中的點(也是已經被從堆疊中拿出的),
> > 代表它們到這邊也沒有路徑;所以不會有任何點可以再擴大這個強連通子圖了。
```cpp!
class SCC {
int d;
vector<int> dfn{}, low{};
vector<int> sccno{};
vector<vector<int>> scc{};
stack<int> s{};
void dfs(const vector<vector<int>>& adj, int u) {
low[u] = dfn[u] = ++d;
s.emplace(u);
for (auto& v : adj[u]) {
if (sccno[v]) continue; // cross edge
if (!dfn[v]) { // tree edge
dfs(adj, v);
low[u] = min(low[u], low[v]);
} else low[u] = min(low[u], dfn[v]); // forward / back / cross edge
}
if (low[u] == dfn[u]) { // first vertex of a scc
scc.emplace_back();
while (true) {
int x{s.top()}; s.pop();
sccno[x] = scc.size(), scc.back().push_back(x);
if (x == u) break;
}
}
}
public:
vector<vector<int>> operator()(const vector<vector<int>>& adj) {
int n{adj.size()};
d = 0, dfn.assign(n, 0), low.resize(n), sccno.assign(n, 0);
for (int i{0}; i < n; ++i) if (!dfn[i]) dfs(adj, i);
return move(scc);
}
};
```
> 因為無向圖找 BCC 時,不會有 cross edge,
> 不管用 $dep$ 或 $dfn$ 都可以,只要父節點與子節點的大小關係正確就好。
> 但是有向圖找 SCC 的時候不一樣,只能用 $dfn(v)$,不然這個方法會行不通。
---
# 最低共同祖先(LCA, lowest common ancestor)
> **lowest 在這邊是 deepest(也可當作地位最低的)**;
> 上面的 low 則是指數字小的。
LCA 是指,一棵樹上的共同祖先中,深度最大的,也就是最靠近的那個共同祖先。
```graphviz
digraph {
A [label="A"];
B [label="B", style=filled, fillcolor=dimgray];
C [label="C", style=filled, fillcolor=lightgray];
D [label="D"];
E [label="E", style=filled, fillcolor=lightgray];
F [label="F"];
A -> B;
B -> C;
B -> D;
D -> E;
A -> F;
}
```
> $C$ 與 $E$ 的 LCA 是 $B$。
假設樹一樣存在鄰接陣列,並已知樹根,
並另外紀錄 $d$ 陣列,代表節點深度。
> 樹根的深度(depth)為 $0$。
要找 $lca(u, v)$,最簡單的方法就是,先從 $u$ 往上走到樹根,
接下來從 $v$ 也往上走到樹根,第一個遇到 $u$ 也走過的節點就是了,
複雜度 $O(H)$。($H$ 為樹高)
<hr class="dashed">
## pointer jumping technique (binary lifting)
我們先把剛剛的暴力解轉換一下,不失一般性的假設 $d(u)\le d(v)$,
我們先從 $v$ 往上走到與 $u$ 同高度,接著 $u, v$ 再一起往上走,直到交會為止。
1. $v$ 往上走 $d(v)-d(u)$ 步
2. $u, v$ 往上走 $d(lca(u,v))-d(v)$ 步
這邊問題就可以轉化為二個步驟,而且都有明確的起點與距離。
接著我們可以用類似二分搜的概念來加速,假設樹高小於 $32$,
我們先看距離有沒有超過 $16$,有的話就往上走 $16$ 步;
剩下的距離一定小於 $16$,我們再看它有沒有超過 $8$;
以此類推,$4,\ 2,\ 1$,就到終點了。
> 雖然不知道 $d(lca(u,v))-d(v)$ 到底是多長,
> 可是透過 $u,v$ 分別走某個距離到達的點,
> 就可以判斷是否已經走到或超過 $lca(u,v)$ 了。
定義 $an[u][i]$ 為 $u$ 往上走 $2^i$ 步到達的祖先。
> 如果超過樹根則設為 $-1$。
```cpp!
class LCA {
const vector<vector<int>>& adj;
int n;
vector<int> d;
vector<int> log2;
vector<vector<int>> an{};
void dfs(int u, int w = -1, int dep = 0) {
d[u] = dep;
an[u][0] = w;
for (int i{1}; i <= log2[n - 1] && an[u][i - 1] != -1; ++i)
an[u][i] = an[an[u][i - 1]][i - 1]; // 走 2^(i-1) 再走 2^(i-1) 步
// 因為計算 an 會用到祖先的資訊,所以先計算再繼續往下
for (auto& v : adj[u]) {
if (v == w) continue; // parent
dfs(v, u, dep + 1);
}
}
public:
LCA(const vector<vector<int>>& _adj, int root)
: adj{_adj}, n{adj.size()}, d(n), log2(n) {
log2[1] = 0;
for (int i{2}; i < log2.size(); ++i) log2[i] = log2[i / 2] + 1;
an.assign(n, vector<int>(log2[n - 1] + 1, -1));
dfs(root);
}
int operator()(int u, int v) {
if (d[u] > d[v]) swap(u, v);
for (int i{log2[d[v] - d[u]]}; i >= 0; --i)
if (d[v] - d[u] >= (1 << i)) v = an[v][i];
// v 先走到跟 u 同高度
if (u == v) return u;
for (int i{log2[d[u]]}; i >= 0; --i)
if (an[u][i] != an[v][i]) u = an[u][i], v = an[v][i];
// u, v 一起走到 lca(u, v) 的下方
return an[u][0];
// 回傳 lca(u, v)
}
};
```
> 這裡初始化 `an`,複雜度就要 $O(|V|\log |V|)$,
> 其實可以先算出樹高,不要直接用到最大可能的樹高 $n-1$,
> 甚至可以每個 `an[u]` 依照 $u$ 的深度開不同長度,
> 但是複雜度還是一樣,常數比較小而已。
前處理複雜度 $O(|V|\log H)$。
<hr class="thin">
> 如果 $u$ 往上走 $2^i$ 步超過樹根,就讓 $an[u][i]$ 等於 $-1$。
查詢複雜度 $O(\log H)$。
> 因為我們沒有辦法區別 $lca(u,v)$ 與它的祖先,
> 所以我們先走到 lca(u, v) 的下方,最後再回傳它的父節點(`an[u][0]`)。
<hr class="dashed">
## Tarjan's offline LCAs algorithm
> offline algorithm 代表一開始就要知道所有的問題(有哪些 $lca(u,v)$),
> 不然就得把全部 $O(|V|^2)$ 個問題答案算好。
假設二點 $u,v$,$v$ 比 $u$ 先被拜訪到,而當拜訪到 $u$ 時,有以下的結論:
> **以下討論都是基於 DFS 剛拜訪到 $u$ 時的狀態**
如果 $v$ 不是 $u$ 的祖先,$v$ 一定處於拜訪完的狀態,
從 $v$ 往上找,第一個處於拜訪中的節點,就是 $lca(u,v)$。
我們還可以發現,$v$ 周圍拜訪完的任意節點 $w$,$lca(u,w)=lca(u,v)$,
而這些節點會形成一棵 $lca(u,v)$ 的子樹[^subtree2]。
> 若 $v$ 是 $u$ 的祖先,那就更簡單了,$lca(u,v)=v$。
只有那些拜訪中的節點是 $u$ 的祖先,也只有這些點有可能是 $lca(u,v)$,
所以對於其中某個節點 $x$,令 $x$ 與 $x$ 所有拜訪完畢的子樹所形成的集合為 $S$,
則對於 $S$ 中的任意一點 $v$,都會使得 $lca(u,v)=x$;
我們要做的就是當每棵子樹被拜訪完畢時,把它與父節點進行合併,
而對於某個節點 $v$,$lca(u,v)$ 就是 $v$ 所在集合的 $x$。
> 另一種觀點,從某個點 $x$ 的角度來看,二個子孫節點 $u,v$,$lca(u,v)$ 是否是 $x$?
> 如果 $u,v$ 分屬於 $x$ 的二棵不同的子樹,則 $lca(u,v)=x$;
> 如果 $u,v$ 在 $x$ 的同一棵子樹,則 $lca(u,v)\neq x$。
>
> > 當然如果 $u$ 或 $v$ 等於 $x$,$lca(u,v)=x$。
>
> 當我們拜訪到 $u$ 時,如果 $v$ 所在的子樹已經拜訪完畢,
> 代表 $u,v$ 分屬於不同子樹,而 $lca(u,v)=x$,
> 因此我們把拜訪完畢的子樹與 $x$ 合併,代表這些節點與 $u$ 的 LCA 都是 $x$。
```graphviz
digraph {
A [label="A", style=dashed];
B [label="B", style=dashed];
C [label="C"];
D [label="D"];
E [label="E", style="dashed, filled", fillcolor=dimgray];
F [label="F"];
G [label="G"];
H [label="H(v)", style=filled, fillcolor=lightgray];
I [label="I"];
J [label="J", style=dashed];
K [label="K(u)", style="dashed, filled", fillcolor=lightgray];
L [label="L", style=dotted];
M [label="M", style=dotted];
N [label="N", style=dotted];
O [label="O", style=dotted];
P [label="P", style=dotted];
A -> B;
B -> C;
C -> D;
B -> E;
E -> F;
F -> G;
F -> I;
G -> H;
E -> J;
J -> K;
K -> L;
K -> M;
J -> N;
N -> O;
A -> P;
}
```
因為每個 $x$ 形成的集合都是沒有交集的,我們可以用併查集來快速運算。
> 為了要進行 weighted union,額外開一個 $ancestor$ 陣列,
> 紀錄每個集合真正的樹根。
```cpp!
class LCA {
DSU dsu{0};
vector<bool> vis{};
vector<int> ancestor{};
vector<vector<int>> que{};
vector<tuple<int, int, int>> ans{};
void dfs(const vector<vector<pair<int, long long>>>& adj, int u) {
for (auto& v : que[u])
if (vis[v]) ans.emplace_back(u, v, ancestor[dsu.find(v)]);
vis[u] = true;
ancestor[dsu.find(u)] = u;
for (auto& [v, _] : adj[u]) {
if (vis[v]) continue;
dfs(adj, v);
dsu.unionn(u, v);
ancestor[dsu.find(u)] = u;
}
}
public:
vector<tuple<int, int, int>> operator()(const vector<vector<pair<int, long long>>>& adj, int root, vector<pair<int, int>>& query) {
int n{adj.size()};
que.assign(n, {});
for (auto& [u, v] : query) que[u].push_back(v), que[v].push_back(u);
dsu.reset(n), vis.assign(n, false), ancestor.resize(n);
dfs(adj, root);
return move(ans);
}
};
```
複雜度 $O(|V|+P\cdot\alpha(|V|))$。
> $P$ 為問題數量。
> $\alpha()$ 是 inverse Ackermann function,
> 是個成長非常緩慢的函數,基本上可視為常數。
<hr class="dashed">
## Euler Tour Technique
> 歐拉行跡(euler trail):剛好經過每個邊一次的行跡。
將樹看成有向圖,並且每條樹邊變為二條有向邊,於是樹就可以被表示成一個歐拉迴路。
> 這樣的表示法被稱為 Euler tour representation (ETR)。
```graphviz
digraph {
A [label="A"];
B [label="B", style=filled, fillcolor=dimgray];
C [label="C", style=filled, fillcolor=lightgray];
D [label="D"];
E [label="E"];
F [label="F"];
G [label="G"];
H [label="H"];
I [label="I"];
J [label="J", style=filled, fillcolor=lightgray];
K [label="K"];
A -> B;
B -> A [style=tapered];
B -> C;
C -> B [color=red, style=tapered];
C -> D [color=red];
D -> C [color=red, style=tapered];
C -> E [color=red];
E -> C [color=red, style=tapered];
B -> F [color=red];
F -> B [color=red, style=tapered];
F -> G [color=red];
G -> F [color=red, style=tapered];
G -> H [color=red];
H -> G [color=red, style=tapered];
B -> I [color=red];
I -> B [style=tapered];
I -> J [color=red];
J -> I [style=tapered];
A -> K;
K -> A [style=tapered];
}
```
```graphviz
digraph {
path [shape=none, label=
<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
<TR>
<TD BORDER="0">euler: </TD>
<TD WIDTH="20">..</TD>
<TD WIDTH="20">C</TD>
<TD WIDTH="20">D</TD>
<TD WIDTH="20">C</TD>
<TD WIDTH="20">E</TD>
<TD WIDTH="20">C</TD>
<TD WIDTH="20">B</TD>
<TD WIDTH="20">F</TD>
<TD WIDTH="20">G</TD>
<TD WIDTH="20">H</TD>
<TD WIDTH="20">G</TD>
<TD WIDTH="20">F</TD>
<TD WIDTH="20">B</TD>
<TD WIDTH="20">I</TD>
<TD WIDTH="20">..</TD>
</TR> <TR>
<TD BORDER="0">depth: </TD>
<TD WIDTH="20">..</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">3</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">3</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">1</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">3</TD>
<TD WIDTH="20">4</TD>
<TD WIDTH="20">3</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">1</TD>
<TD WIDTH="20">2</TD>
<TD WIDTH="20">..</TD>
</TR>
</TABLE>>
];
}
```
> 一樣不失一般性地假設 $v$ 先被拜訪,再來才是 $u$。
從第一次經過 $v$,到第一次經過 $u$,中間的路徑完全落在子樹 $lca(u,v)$ 中;
而因為 $u,v$ 分屬於 $lca(u,v)$ 不同的子樹,所以一定會經過 $lca(u,v)$。
而子樹 $lca(u,v)$ 中,深度最小的點自然就是 $lca(u,v)$,
於是要找 $lca(u,v)$,可以轉化為尋找,
**第一次經過 $v$ 到第一次經過 $u$ 路徑上「深度最小的點」**。
> 這是一個 RMQ 問題,不用支援修改,可以用**稀疏表(sparse table)**。
```cpp!
class LCA {
const vector<vector<int>>& adj;
int n;
vector<int> d, first, euler{}, log2{};
vector<vector<int>> st{};
void dfs(int u, int w = -1, int dep = 0) {
d[u] = dep;
first[u] = euler.size();
euler.push_back(u);
for (auto& v : adj[u]) {
if (v == w) continue;
dfs(v, u, dep + 1);
euler.push_back(u);
}
}
public:
LCA(const vector<vector<int>>& _adj, int root) : adj{_adj}, n{adj.size()}, d(n), first(n) {
dfs(root);
int tn{euler.size()};
log2.resize(tn + 1);
log2[1] = 0;
for (int i{2}; i <= tn; ++i) log2[i] = log2[i / 2] + 1;
st.assign(tn, vector<int>(log2[tn] + 1));
for (int i{tn - 1}; i >= 0; --i) {
st[i][0] = euler[i];
for (int j{1}; i + (1 << j) <= tn; ++j) {
auto& x{st[i][j - 1]};
auto& y{st[i + (1 << (j - 1))][j - 1]};
st[i][j] = d[x] <= d[y] ? x : y;
}
}
}
int operator()(int u, int v) {
int l{first[u]}, r{first[v]};
if (l > r) swap(l, r);
++r; // make the interval left closed right open
int j{log2[r - l]};
auto& x{st[l][j]};
auto& y{st[r - (1 << j)][j]};
return d[x] <= d[y] ? x : y;
}
};
```
前處理複雜度 $O(|V|\log |V|)$,詢問複雜度 $O(1)$。
> 或是用線段樹,可以達到 $O(|V|)$ 前處理,$O(\log |V|)$ 詢問。
<hr class="dashed">
## 樹鏈剖分(heavy-light decomposition)
```graphviz
graph {
node [shape=circle, style=filled, label=""];
node[fillcolor=orange];
D;
node[fillcolor=red];
A; B; C; E; F;
node[fillcolor=yellow];
G;
node[fillcolor=green];
H; I; J;
node[fillcolor=blue];
K; L;
node[fillcolor=indigo];
M;
node[fillcolor=purple];
N;
edge [style=bold];
A -- B -- C -- E -- F;
H -- I -- J;
K -- L;
edge [style=dotted];
C -- D;
B -- G;
A -- H;
A -- K;
K -- M;
K -- N;
}
```
樹鏈剖分是一個將樹分解為一堆路徑的技巧;
其精神在於「**重鏈輕剖**」,鏈結重的子樹,剖去輕的子樹。
對於每個節點 $u$,選擇一個子節點 $v$,子樹 $v$ 是所有子樹最重的(節點最多),
稱這樣的邊為重邊(heavy edge),其餘則為輕邊(light edge);
每個非葉節點,都通過重邊與一個子節點相連,形成一堆由重邊組成的路徑(heavy path)。
對於二個節點 $u,v$,如果 $u,v$ 在不同的路徑上,
而 $v$ 所在的路徑起點 $s(v)$ 比 $s(u)$ 比較深,
有可能 $u$ 是 $s(v)$ 的祖先,也有可能 $u$ 跟 $s(v)$ 沒有關係,
但 $s(v)$ 不會是 $u$ 的祖先,所以 $lca(u,v)=lca(u,s(v))$;
如果 $u,v$ 在同個路徑上,而 $v$ 的深度比較深,則 $lca(u,v)=u$。
```cpp!
class LCA {
const vector<vector<int>>& adj;
int n;
vector<int> pa, dep, heavy, head;
// 父節點, 深度, 重邊, 路徑起點
int heavy_light(int u, int w = -1, int d = 0) {
pa[u] = w, dep[u] = d;
int mx{0}, tot{1};
for (auto& v : adj[u]) {
if (v == w) continue;
int sz{heavy_light(v, u, d + 1)};
if (sz > mx) mx = sz, heavy[u] = v;
tot += sz;
}
return tot;
}
void decompose(int u, int h) {
head[u] = h;
for (auto& v : adj[u]) {
if (v == pa[u]) continue;
if (v == heavy[u]) decompose(v, h);
else decompose(v, v);
}
}
public:
LCA(const vector<vector<int>>& _adj, int root) : adj{_adj}, n{adj.size()}, pa(n), dep(n), heavy(n), head(n) {
// Heavy-Light Decomposition
heavy_light(root);
decompose(root, root);
}
int operator()(int u, int v) {
while (head[u] != head[v]) {
if (dep[head[u]] <= dep[head[v]]) v = pa[head[v]];
else u = pa[head[u]];
}
return dep[u] <= dep[v] ? u : v;
}
};
```
每次透過輕邊到別條路徑上,路徑起點從 $s$ 變為 $s'$,
子樹 $s'$ 至少是子樹 $s$ 的二倍重,所以至多 $O(\log_2n)$ 次到達根節點;
複雜度為 $O(\log n)$。
---
# References
* 演算法筆記 - [Component](http://web.ntnu.edu.tw/~algo/Component.html)
* Wikipedia - [Biconnected component](https://en.wikipedia.org/wiki/Biconnected_component)
* Wikipedia - [Tarjan's strongly connected components algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm)
* CP-Algorithms - [Lowest Common Ancestor - Tarjan’s off-line algorithm](https://cp-algorithms.com/graph/lca_tarjan.html)
* Wikipedia - [Euler tour technique](https://en.wikipedia.org/wiki/Euler_tour_technique)
* CP-Algorithms - [Heavy-light decomposition](https://cp-algorithms.com/graph/hld.html)
---
# 練習題
| Problem | Tags |
|:-:|:- |
| [Network](https://onlinejudge.org/index.php?option=onlinejudge&page=show_problem&problem=251) | `AP` |
| [Street Directions](https://onlinejudge.org/index.php?option=onlinejudge&page=show_problem&problem=551) | `bridge` |
| [Mining Your Own Business](https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=3136) | `BCC` |
| [Lowest Common Ancestor of a Binary Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree) | `LCA` |
| [血緣關係](https://zerojudge.tw/ShowProblem?problemid=d767) | `LCA` |
[^subtree]: **子樹 $v$ 代表以 $v$ 為樹根的子樹(subtree rooted at v)。**
[^subtree2]: **$u$ 的子樹代表以 $u$ 的子節點為樹根的子樹(subtree rooted at v, which is a child of u)。**
[^wikiBCC]: [Wikipedia File:Graph-Biconnected-Components.svg](https://en.wikipedia.org/wiki/File:Graph-Biconnected-Components.svg)
<style>
hr.thin {
height: 0.5px;
}
hr.dotted {
border-top: dotted;
height: .0em;
color: #777777;
background-color: #ffffff;
}
hr.dashed {
border-top: dashed;
height: .0em;
color: #777777;
background-color: #ffffff;
}
/* Gradient transparent - color - transparent */
hr.gradient {
border: 0;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
}
/* Flaired edges, by Tomas Theunissen */
hr.flaired {
overflow: visible;
height: 30px;
border-style: solid;
border-color: black;
border-width: 1px 0 0 0;
border-radius: 20px;
background-color: #ffffff;
}
hr.flaired:before { /* Not really supposed to work, but does */
display: block;
content: "";
height: 30px;
margin-top: -31px;
border-style: solid;
border-color: black;
border-width: 0 0 1px 0;
border-radius: 20px;
}
</style>