跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 強連通分量

強連通分量

有向圖裡彼此都走得到的點會聚成一團:Tarjan 和 Kosaraju 只用一兩次 DFS(Depth-First Search,深度優先搜尋)就能找出每一團,把每團縮成一個點後,圖就變成有向無環圖。

圖
1/31
A0BCDEFGH
目前的點在堆疊上已屬於某個分量連回堆疊的邊
點ABCDEFGH
編號(第幾個走到)0·······
low(繞得回的最小編號)0·······
堆疊:底 → 頂
  1. 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)
縮點成 DAGO(V + E)O(V + E)

空間:O(V),編號、low、堆疊;Kosaraju 另外要 O(V + E) 存反向圖

Big O 實測:n 變大時步數怎麼長

數的是:走訪點+看邊的次數(每點約 4 條出邊的隨機有向圖)

Big On = 250n = 2,500n = 25,000成長倍數:實測(理論)
TarjanO(n)1,24212,492124,991×101 (×100)
KosarajuO(n)2,73427,484274,982×101 (×100)

邊數和點數成正比,O(V + E) 就是 O(n):點變一百倍,步數也一百倍。

和其他做法比

邊數Tarjan:步數Kosaraju:步數分量數最大的分量
250 個點9921,2422,7349242
2,500 個點9,99212,49227,484632,438
25,000 個點99,991124,991274,98251024,491

隨機有向圖、每個點約 4 條出邊,兩種做法都實際跑過,步數=走訪點的次數加上看邊的次數。答案一樣,Kosaraju 大約多一倍:它要做兩次深度優先搜尋,還要先建一張反向圖。隨機圖上大部分點會落在同一個巨大分量裡,剩下的是零星的單點。

真實世界裡的它

  • 2-SAT(每個子句兩個變數的布林可滿足性問題):x 和 非x 落在同一個分量裡就無解。
  • 找出程式模組之間的循環依賴,整組一起處理或拆開。
  • 網頁連結圖、社群網路的「核心」:彼此互相連得到的那一大群。
  • 編譯器分析控制流程圖裡的迴圈,以及垃圾回收器找出互相參照的物件群。

取捨與陷阱

  • 看到已經走過的點就拿它的編號更新 low:要先確定它還在堆疊上。已經屬於別的分量的點,走得過去也回不來。
  • 遞迴寫法在幾萬個點排成一長串的圖上會 stack overflow(Python 預設上限只有 1,000 層);上方示範的引擎用明確的堆疊,5 萬個點的長鏈也沒問題。
  • 把有向圖當無向圖處理:「連通」和「強連通」不同,A → B 不代表 B 走得回 A。

LeetCode 練習