進階主題 · 圖論

拓樸排序 Topological Sort

把一堆有先後相依的事情,排成一條不違反任何順序的直線。排課、編譯、任務排程都靠它。

有些事情有先後相依:資料結構要先修完程式設計、編譯 B 之前得先編好它依賴的 A、穿鞋之前要先穿襪子。拓樸排序就是把這些節點排成一條線,讓每一條箭頭 u→v 都滿足 u 排在 v 前面。前提很重要:圖必須是 DAG(有向、無環);一旦有環(A 要先於 B、B 又要先於 A),就先有雞還先有蛋,排不出來。合法順序通常不只一種。

核心觀念:Kahn 入度法

入度(in-degree) —— 有幾條箭頭「指向」這個節點。入度 0 = 沒有任何先決條件,現在就能做。

1. 入度 0 的先進場 —— 把所有入度 0 的節點丟進佇列。

2. 取一個排出,砍它的出邊 —— 從佇列拿一個放進結果,對它每個下游鄰居入度減 1(它的先決條件少了一個)。

3. 下游入度歸零就進場 —— 某個鄰居入度減到 0,代表它的先決都排好了,加進佇列。

4. 排出數 < 節點數 = 有環 —— 佇列空了卻還有節點沒排出,代表它們卡在環裡(入度永遠歸不了零)。

還有另一招:DFS 後序法。對每個節點做 DFS,等它的子孫全部處理完才把自己壓進堆疊,最後整個反轉就是拓樸順序。兩種做法都是 O(V+E);Kahn 的好處是順便偵測環、也方便控制輸出順序(想要字典序最小就把佇列換成最小堆)。

複雜度

O(V+E)時間 —— 每個節點進出佇列一次、每條邊被「砍」一次,線性。
O(V)空間 —— 入度陣列 + 佇列 + 結果串列,節點級。
O(V+E)建鄰接表 —— 讀一遍所有邊,建出「誰指向誰」與入度。
它其實是「有方向的 BFS」。把 BFS 的「一層層擴散」換成「入度歸零就放行」,就是 Kahn。難怪兩者都是 O(V+E)。

用生活比喻它

📚

修課先修順序

資料結構要先修程式設計、演算法要先修資料結構。排出一張「照這順序修一定不卡」的課表。

🔨

編譯 / 建置依賴

Makefile、npm 裝套件:先把你依賴的東西建好 / 裝好,才輪到你。順序錯就編不過。

👕

穿衣服的順序

先襪子後鞋子、先內衣後外套;但襪子跟內衣誰先都行 —— 所以合法順序不只一種。

🎮 Kahn 入度法動畫

節點上方數字是入度(指向它的箭頭數)。每步從佇列(琥珀)取一個排出(紫色正在排出的),砍掉它的出邊、下游入度減 1,減到 0 就進佇列,排出的變綠色。切「有環」情境看 Kahn 怎麼卡住。按「下一步」。

情境:
佇列(入度=0,等著出列):
拓樸順序(已排出):

10 個程式碼範例(Python)

從 Kahn 入度法、偵測環、課程表能不能修完、DFS 後序法,到字典序最小、DAG 最長/最短路、有向圖偵測環、列出所有拓樸序,最後一張應用小結。

顯示範例:

常見陷阱

❌ 圖有環還想拓樸排序 —— 根本不存在合法順序。務必先確認是 DAG;用 Kahn 時「排出數 < 節點數」就是有環的信號。

❌ 以為答案唯一 —— 平手的節點(同時入度 0)誰先都行,通常有多種合法順序。要固定結果就指定規則(如字典序最小)。

❌ 邊的方向搞反 —— u→v 代表「u 要在 v 前面」(u 是 v 的先決)。方向反了整個順序就反,課程表就變成「先修進階再修基礎」。

❌ DFS 法壓堆疊時機錯 / 忘了反轉 —— 一定要在後序(子孫都處理完)才把自己壓進去,最後整個反轉才是拓樸順序。

🎯 換你了(先別急著查答案)

把答案打到對話裡,我幫你對。