強連通分量
有向圖裡彼此都走得到的點會聚成一團:Tarjan 和 Kosaraju 只用一兩次 DFS(Depth-First Search,深度優先搜尋)就能找出每一團,把每團縮成一個點後,圖就變成有向無環圖。
圖
1/31
目前的點在堆疊上已屬於某個分量連回堆疊的邊
| 點 | A | B | C | D | E | F | G | H |
|---|---|---|---|---|---|---|---|---|
| 編號(第幾個走到) | 0 | · | · | · | · | · | · | · |
| low(繞得回的最小編號) | 0 | · | · | · | · | · | · | · |
堆疊:底 → 頂
- A
強連通分量(找到的順序)
(還沒有)走到 A:編號 0(第幾個走到),low 暫時也是 0。把它推進堆疊。
縮點後的圖:每個分量縮成一個點,一定沒有環。
搜尋完成後畫出來。
分量數
0
堆疊大小
1
走訪+看邊的次數
1
亮起來的是這一步執行的程式碼
function tarjan(adj: number[][]): number[][] { const n = adj.length; const index: (number | null)[] = new Array(n).fill(null); const low: number[] = new Array(n).fill(0); const onStack: boolean[] = new Array(n).fill(false); const stack: number[] = []; const components: number[][] = []; let counter = 0; function visit(v: number): void { index[v] = low[v] = counter++; stack.push(v); onStack[v] = true; for (const w of adj[v]) { if (index[w] === null) { visit(w); low[v] = Math.min(low[v], low[w]); } else if (onStack[w]) { low[v] = Math.min(low[v], index[w]!); } } if (low[v] === index[v]) { const component: number[] = []; let w: number; do { w = stack.pop()!; onStack[w] = false; component.push(w); } while (w !== v); components.push(component); } } for (let v = 0; v < n; v++) if (index[v] === null) visit(v); return components;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 想知道有向圖裡「哪些點彼此互相走得到」:同一個強連通分量裡,任兩點都能走到對方。
- 要在有環的圖上做只適用於 DAG(Directed Acyclic Graph,有向無環圖)的事,例如拓撲排序或動態規劃:先把每個分量縮成一個點,剩下的一定是 DAG。
- Tarjan 只要一次 DFS、不需要反向圖;Kosaraju 概念比較好懂,兩次 DFS 各做一件簡單的事。
和其他主題的關係
- 延伸閱讀
- 拓撲排序
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| Tarjan 一次深度優先搜尋,每個點進出堆疊一次 | O(V + E) | O(V + E) |
| Kosaraju 兩次搜尋,加上反向圖 | O(V + E) | O(V + E) |
| 縮點成 DAG | O(V + E) | O(V + E) |
空間:O(V),編號、low、堆疊;Kosaraju 另外要 O(V + E) 存反向圖
Big O 實測:n 變大時步數怎麼長
數的是:走訪點+看邊的次數(每點約 4 條出邊的隨機有向圖)
| Big O | n = 250 | n = 2,500 | n = 25,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|
| Tarjan | O(n) | 1,242 | 12,492 | 124,991 | ×101 (×100) |
| Kosaraju | O(n) | 2,734 | 27,484 | 274,982 | ×101 (×100) |
邊數和點數成正比,O(V + E) 就是 O(n):點變一百倍,步數也一百倍。
和其他做法比
| 邊數 | Tarjan:步數 | Kosaraju:步數 | 分量數 | 最大的分量 | |
|---|---|---|---|---|---|
| 250 個點 | 992 | 1,242 | 2,734 | 9 | 242 |
| 2,500 個點 | 9,992 | 12,492 | 27,484 | 63 | 2,438 |
| 25,000 個點 | 99,991 | 124,991 | 274,982 | 510 | 24,491 |
隨機有向圖、每個點約 4 條出邊,兩種做法都實際跑過,步數=走訪點的次數加上看邊的次數。答案一樣,Kosaraju 大約多一倍:它要做兩次深度優先搜尋,還要先建一張反向圖。隨機圖上大部分點會落在同一個巨大分量裡,剩下的是零星的單點。
真實世界裡的它
- 2-SAT(每個子句兩個變數的布林可滿足性問題):x 和 非x 落在同一個分量裡就無解。
- 找出程式模組之間的循環依賴,整組一起處理或拆開。
- 網頁連結圖、社群網路的「核心」:彼此互相連得到的那一大群。
- 編譯器分析控制流程圖裡的迴圈,以及垃圾回收器找出互相參照的物件群。
取捨與陷阱
- 看到已經走過的點就拿它的編號更新 low:要先確定它還在堆疊上。已經屬於別的分量的點,走得過去也回不來。
- 遞迴寫法在幾萬個點排成一長串的圖上會 stack overflow(Python 預設上限只有 1,000 層);上方示範的引擎用明確的堆疊,5 萬個點的長鏈也沒問題。
- 把有向圖當無向圖處理:「連通」和「強連通」不同,A → B 不代表 B 走得回 A。