# 拓樸排序 Topological Sort
拓樸排序是一種幫一張圖上的節點做線性排序的演算法。對於一張有向無環圖中的所有節點 $V(G) = \{v_1,v_2,...,v_{|V|}\}$,排出一個序列,使所有有向邊 $(u,v) \in E(G)$ 在序列中 $u$ 必排在 $v$ 之前
## 為什麼需要拓樸排序?
為什麼會需要這個演算法呢? 可以考慮以下問題:
### 引入情境
:::info
已知資工系有 $10$ 門課程:
|課程 1|課程 2|課程 3|課程 4|課程 5|
| ----- | ----- | ----- | ----- | ----- |
|計算機概論|電腦網路|離散數學|微積分|演算法|
|課程 6|課程 7|課程 8|課程 9|課程 10|
| ----- | ----- | ----- | ----- | ----- |
|深度學習|機率論|資料結構|計算機組織|數位邏輯|
每一門課程都有優先修習的順序,如下圖
```graphviz
digraph hierarchy {
size ="4,4"
計算機概論 -> 資料結構 -> 演算法 -> 深度學習
離散數學 -> 數位邏輯 -> 計算機組織
離散數學 -> 資料結構
演算法 -> 電腦網路
微積分 -> 機率論 -> 深度學習
}
```
以上圖為例,如果想要修習「演算法」,就得先修「資料結構」... 以此類推
如果某大一新生想排修課順序把 $10$ 門課都修完,該如何排安排才能符合以上規則呢?
:::
### 解法
我們把問題拆解一下:
假設我只想修「資料結構」,那麼先決條件就是需要先修完「離散數學」與「計算機概論」
在此問題中,其中一種合理的順序是:
**<font color="red">計算機概論</font>、微積分、離散數學、數位邏輯、資料結構、計算機組織、演算法、電腦網路、機率論、深度學習**
當然,我們也可以發現答案不只一種,像是以下也可以是一種解答:
**微積分、<font color="red">計算機概論</font>、離散數學、數位邏輯、資料結構、計算機組織、演算法、電腦網路、機率論、深度學習**
### 小結
這個問題相當簡單,看一下就能知道答案。因為它只有 $10$ 門課,在圖論而言,就是 $10$ 個節點
但是如果今天問題變成有 $10^{6}$ 個節點呢? 太多了吧!!
這顯然需要一套「規則」去解決這個問題
## 介紹
### 目標
拓樸排序可以為一張是 $dag$ 的圖 $G$ 排出適當的走訪路徑,使對於 $G$ 的所有邊 $(u,v)$,$u$ 必出現在 $v$ 之前
### 輸入/輸出
- 輸入為一張 $dag$
- 輸出則為一個陣列,紀錄節點的順序
### 備註
- **拓樸排序以 $dag$ 作為輸入**,這是一個定理,在機制的部分會詳細說明
- $dag$ 為有向無環圖 directed acyclic graph 的簡稱
### 拓樸???
沒有,這一點拓樸學也沒有。在古早時代的「拓樸」一詞涵蓋的非常廣泛,基本上只要是非數字或幾何的東西都可能被冠上拓樸之稱。早期的圖論也被認為是拓樸學的內容,而現在則不一定。像是拓樸排序所排的節點順序就不是基於數字的大小關係,因此才會有「拓樸」二字出現
## 原理說明
### 觀察 1: 入度與 $dag$ 的關係
對於 $G$ 的所有邊 $(u,v)$,$u$ 必出現在 $v$ 之前。因此必須最先選擇入度為 $0$ 的節點,因為入度為 $0$ 的節點**沒有任何邊通往此節點**
接下來要走訪鄰居節點,但是要怎麼走訪呢? 可以試著把已走訪過的節點從圖上「**刪除**」,如此一來,就會有新的入度為 $0$ 的節點了
以下圖為例
我們可以將 $a$ 刪除,如此一來就知道接下來要走訪 $b$,因為 $b$ 的入度為 $0$ (沒有結點走到 $b$)
```graphviz
digraph hierarchy {
node [shape = "circle"]
size = "3,3"
rankdir = LR
a -> b
a -> c -> d
{rank=same b -> c}
}
```
<p class = "text-center">
⭣
</p>
```graphviz
digraph hierarchy {
node [shape = "circle"]
size = "2,3"
rankdir = LR
c -> d
{rank=same b -> c}
}
```
**入度: 對於一有向圖,設一節點 $v$,則 $v$ 的入度即為所有通往 $v$ 的邊數**
### 觀察 2: 深度優先 (DFS) 走訪順序
在先前探討 [DFS 與 BFS](https://hackmd.io/@ShanC/bfs_dfs) 中,有提及 DFS 的「時間戳記」性質
1. 在 DFS 森林中,如果有 $(u,v)$ 存在,就不會出現 $(v,u)$,因為每個節點只被拜訪一次
2. 時間戳記中,有一陣列 $f[i]$ 紀錄節點 $i$ 完成拜訪所有鄰居的時間
3. 當一節點 $v$ 完成拜訪,即 $v$ 的所有在 DFS 森林中的所有後代都已**完成拜訪**
4. 會發現以完成時間順序由大到小排所有節點 $v$,即為拓樸排序
## 拓樸排序的機制 (Kahn's Algorithm)
首先,這個演算法名稱是[這樣](https://youtu.be/GpFO5vfOjhI?si=T9ORP4zuoGAxrpPH)唸
### 說明
step1. 第一個點必不會排在其他點後方,因此要找的 $v$ 必須 $deg(v) = 0$,即入度為 $0$
step2. 將每個入度為 $0$ 的節點走訪過
step3. 刪除每個入度為 $0$ 的節點,此步驟會產生新的入度為 $0$ 的節點
step4. 回到 step1. 直到沒有入度為 $0$ 的節點產生
這想法看起來非常遞迴,實際上用迴圈時做即可,~~比較看得懂~~
### 測環
此方法也可以用於測定是否有環
假設這張有向圖是有環的,則會有可能產生「無法走訪」的節點。因此如果在跑完此演算法之後,發現有節點未被走訪,則這是一張有環有向圖
```graphviz
digraph hierarchy {
size = "3,3"
rankdir = LR
a -> b
b -> c
d -> b
{rank=same c -> d}
}
```
<p class = "text-center">
⭣ 完成拓樸排序後 ⭣
</p>
```graphviz
digraph hierarchy {
size = "3,3"
rankdir = LR
b -> c
d -> b
{rank=same c -> d}
}
```
由於沒有任何節點入度為 $0$,因此演算法停止,然而仍有三點未被刪去,因此認定此圖有環
## 拓樸排序的機制 (DFS)
要特別說明一點,要排出順序必須要有「**大小關係**」
### 名詞定義
在 DFS 樹或是森林中:
- 白點: 未被走過的節點
- 灰點: 被走過,但並未拜訪完鄰居的節點
- 黑點: 已拜訪完鄰居的節點
- 後向邊: 設 $u$ 是 $v$ 的後代,則 $u$ 連到 $v$ 的邊為後向邊
### 引理
對有向圖 $G$ 進行 DFS 不產生後向邊,若且為若,$G$ 沒有迴路
### 定理: 拓樸排序算法能為 $dag$ 排出優先順序
假設以 DFS 走訪 $dag$ $G = (V, E)$,若有一條邊 $(u, v)$,則 $f[u] > f[v]$,證明如下:
當 $(u,v)$ 邊被發現時
- 如果 $v$ 是灰點,則 $(u,v)$ 就是一條後向邊,這樣會與引理相違背,因此不可能出現
- 如果 $v$ 是白點,就代表他是 $u$ 的後代,因此 $f[u] > f[v]$
- 如果 $v$ 是黑點,則代表此 $(u, v)$ 早已被走訪完畢了,因此 $f[u] > f[v]$
得證: 對於 $G$ 中的所有邊 $(u,v) \in E$ 而言,$f[u] > f[v]$
由於每個節點都有「**大小關係**」,因此可以排出優先順序
### 測環
根據以上證明,當一條邊 $(u, v)$ 被發現時,如果 $v$ 是灰點,使 $(u, v)$ 為一條後向邊,則此圖有環
## 程式實作
### 刪除節點版本 (Kahn's Algorithm)
在實作上,不需要真的把節點刪除,只需要在走訪節點時將其鄰居的入度 $-1$ 即可
其實 queue 改成 stack 也會產生一組拓樸排序的結果
#### C++ code
```cpp
vector<int> edge[MAXN], ans; // MAXN: 最大節點數
int deg[MAXN]; // 入度
void topo(int n) { // 節點編號為 1-based
queue<int> que; // 以 queue 當作拜訪清單
for (int i = 1; i <= n; i++)
if (!deg[i]) // 如果入度 == 0 則加入此節點
que.push(i);
while (!que.empty()) {
int u = que.front();
que.pop();
ans.push_back(u); // 結果存 ans
for (int v : edge[u]) {
deg[v]--; // 每次走訪完都先幫鄰居的入度 -1
if (!deg[v]) // 如果入度 == 0 則加入此節點
que.push(v);
}
}
}
```
#### 時間複雜度
令節點數 $|V| = n$、邊數 $|E| = m$
- 遍歷整個 $deg[~]$ : $O(n)$
- 遍歷整張圖: $O(n + m)$
- 總時間複雜度: $O(n) + O(n + m) = O(n + m)$
### DFS 版本
#### C++ code
```cpp
vector<int> edge[MAXN], ans;
bool vis[MAXN];
int f[MAXN], time_cnt; // MAXN: 最大節點數
void dfs(int u) {
vis[u] = true;
for (int v : edge[u]) {
if (vis[v])
continue;
dfs(v);
}
f[u] = ++time_cnt; // 紀錄完成拜訪的時間
ans.push_back(u); // 將順序存入 ans
}
void topo(int n) { // 1-base
time_cnt = 0;
for (int i = 1; i <= n; i++) {
if (!vis[i])
dfs(i);
}
reverse(ans.begin(), ans.end()); // 反轉答案
}
```
#### 時間複雜度
令節點數 $|V| = n$、邊數 $|E| = m$
- DFS 整張圖: $O(n + m)$
- 將答案存入 $ans$ : $O(n)$
- 反轉 $ans$ : $O(n)$
- 總時間複雜度: $O(n + m) + O(n) \times 2 = O(n + m)$
## 題目練習
[Zerojudge f167. m4a1-社團 Club](https://zerojudge.tw/ShowProblem?problemid=f167)
[CSES Course Schedule](https://cses.fi/problemset/task/1679)
[UVa 200 - Rare Order](https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=4&page=show_problem&problem=136)
[CSES Game Routes](https://cses.fi/problemset/)
[CSES Longest Flight Route](https://cses.fi/problemset/task/1680) (DP on DAG)
[AtCoder dp_g - G - Longest Path](https://atcoder.jp/contests/dp/tasks/dp_g?lang=en) (DP on DAG)
[Zerojudge k734. APCS 4. 開啟寶盒](https://zerojudge.tw/ShowProblem?problemid=k734)
[Codeforces Contest 1385 E. Directing Edges](https://codeforces.com/contest/1385/problem/E)
[Zerojudge l245. D. 汽車不再繞圈圈](https://zerojudge.tw/ShowProblem?problemid=l245) (二分搜 + 拓排 + 把DFS版本的證明讀熟就會的東西)
----
## 參考資料
- Introduction to Algorithms, Fourth Edition
- [海大競程 圖論](https://hackmd.io/@LeeShoWhaodian/Graph1#/)
- [geeksforgeeks](https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/)
- [directed acyclic graph - 台師大演算法筆記](https://web.ntnu.edu.tw/~algo/DirectedAcyclicGraph.html)
- [SA 流 C++ 競程修練心法](https://hackmd.io/@sa072686/cp/%2F%40sa072686%2FHybjt6suS)
- [Why is "topological sorting" topological?](https://cstheory.stackexchange.com/questions/30659/why-is-topological-sorting-topological)
- [Etymology of "topological sorting"](https://math.stackexchange.com/questions/113288/etymology-of-topological-sorting)
----
> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2024/11/22