影片狀態:錄影需登入或課程授權。 影片來源與說明
這是 Stanford CS161 導讀第 10 篇,對應 Stanford CS161, Winter 2026, Lecture 9。官方課名是 Graphs and BFS and DFS,上課日期為 2026 年 2 月 4 日,講師是 Ellen Vitercik。
本篇依照官方 Lecture 9 頁面、公開 notes 與 slides 整理。Notes 詳寫 graph representation、DFS 與 BFS;slides 還把 DFS 用到 topological sorting、把 BFS 用到 bipartiteness,兩者都屬本講 agenda,不能因 notes 沒展開就漏掉。官方 Colab 沿用 winter2025-extra,本文未用它支撐 Winter 2026 的新主張。Canvas-only 錄影未作為來源。
這堂課的核心不是背兩份走訪 pseudocode,而是看見「探索順序」會留下不同證據。DFS 的 discovery / finish times 把巢狀結構編碼下來;BFS 的 layers 則直接對應 edge 數最少的距離。相同的 O(n+m) 掃描框架,因容器與 bookkeeping 不同,能解不同問題。
先決定圖怎麼放進記憶體
圖寫成 G=(V,E),令 n=|V|、m=|E|。Undirected graph 的 edge 雙向;directed graph 的 (u,v) 只表示 u 指向 v。Sparse graph 的 edges 接近 Θ(n),dense graph 則可能接近 Θ(n²)。
兩種標準表示法有不同成本:
| 表示法 | 空間 | 查 (u,v) | 列出 u 的 neighbors |
|---|---|---|---|
| Adjacency matrix | Θ(n²) | Θ(1) | Θ(n) |
| Adjacency list | Θ(n+m) | 最壞 Θ(deg(u)) | Θ(deg(u)) |
Matrix 用一個 n×n 的 0/1 表格;list 則為每個 vertex 保存一份 neighbors。Directed graph 常把 out-neighbors 與 in-neighbors 分開,因為演算法可能只需要其中一種方向。
後面說 DFS / BFS 是 O(n+m) 時,預設使用 adjacency list。若用 matrix,每 visit 一個 vertex 都掃完整列,即使圖很 sparse 也要 Θ(n²)。Big-O 不只屬於 pseudocode,也屬於 representation。
DFS:走到底再回頭
Depth-first search(DFS)從 source s 出發,每次優先沿尚未探索的 neighbor 繼續深入,直到無路可走才 backtrack。官方 slides 用迷宮、粉筆與繩子的比喻;pseudocode 用三種顏色:
- White:尚未發現。
- Gray:已發現,但仍在遞迴 stack 上,尚未完成所有 neighbors。
- Black:已完成,之後不再展開。
每個 vertex 還記錄 parent p(v)、discovery time d(v) 與 finish time f(v)。流程是:
DFS(s, t):
color(s) = gray
d(s) = t; t++
for v in out-neighbors(s):
if color(v) == white:
p(v) = s
t = DFS(v, t); t++
f(s) = t
color(s) = black
return t
若只從一個 source 跑,結果是它可到達的 vertices。要探索整張可能 disconnected 的圖,就用外層 loop:每遇到仍 white 的 vertex,開一棵新的 DFS tree。所有 trees 合起來是 DFS forest;在 undirected graph 中,每棵 tree 對應一個 connected component。
一個 timestamps 例子
假設從 a 開始,依序深入 a→b→c→d,完成 d 後逐層回退;另一個 component 是 e→f。可能得到:
a: (1,8) b: (2,7) c: (3,6) d: (4,5)
e: (9,12) f: (10,11)
Descendant 的 interval 完整嵌在 ancestor interval 內。這不是裝飾資訊;finish times 會支撐 topological sorting,也會在 Lecture 10 成為 SCC 演算法的核心。
DFS 為什麼是 O(n+m)
每個 vertex 只會從 white 變成 gray 一次,再變 black;因此 node visits 至多 n 次。掃 u 的 adjacency list 時,每個 directed edge (u,v) 被看一次;undirected edge 在兩端 list 各出現一次,所以至多兩次。每次 visit 與 edge scan 只做常數工作,總成本是 O(n+m)。
同一個計數也說明 DFS 不會漏掉 reachable vertex。若 v 從 source 可達,取一條 path;DFS 掃到 path 上每個已發現 vertex 時,都會檢查下一條 edge。尚未發現的下一點會被遞迴進入,已發現則早已在 traversal 中。DFS 只沿真實 edges 移動,也不會憑空到達不可達點。
投影片應用一:拓撲排序
在 package dependencies 或課程先修關係中,我們想找一個順序,使每條 directed edge A→B 都把 A 放在 B 前。這叫 topological order,而且只有 directed acyclic graph(DAG)保證存在。
演算法很短:
- 對整張 DAG 跑 DFS forest並記 finish time。
- 按 finish time 遞減輸出 vertices。
正確性集中在一個命題:若 DAG 有 edge A→B,則 f(B)<f(A)。
- 若 B 是 A 的 DFS descendant,B 必須先完成,遞迴才回到 A,所以
f(B)<f(A)。 - 若 B 不是 A 的 descendant,A 也不可能是 B 的 descendant;否則 B 到 A 的 DFS-tree path 加 edge
A→B會形成 cycle。於是 B 必須在 A 開始前已完成,仍有f(B)<f(A)。
所以 decreasing finish time 一定把 A 放在 B 前。若圖有 cycle,finish times 仍存在,卻不能產生合法 topological order;課程中的 proof 明確使用 DAG 前提。
BFS:一層一層推進 frontier
Breadth-first search(BFS)不是先走到底,而是先完成距 source 一步的 vertices,再完成兩步、三步。定義 L_i 為從 s 出發、距離 i 的 layer:
L₀ = {s}
處理 L_i 時,掃其中每個 u 的 neighbors。第一次看到 x,就標記 visited、設 p(x)=u,並把 x 放進 L_{i+1}。實作上可用 queue:從 front 取最早發現的 vertex,把新 neighbors 放到 back。DFS 的 iterative 版本則使用 stack;兩份 skeleton 的主要差異就是 FIFO 與 LIFO。
對 unweighted graph,path 長度是 edge 數。BFS 不只找 reachable set,還計算 source 到每個 reachable vertex 的最短距離。Parent pointers 形成 BFS tree,從目標沿 parents 回走就能還原一條 shortest path。
為什麼 BFS layers 就是最短距離
要證明的命題是:
L_i = {x | dist(s,x)=i}
用 strong induction。Base case L₀={s}。假設到 i 為止每層都正確,考慮 L_{i+1}。
先證「被放進來的不會太遠或太近」。若 y 被放入 L_{i+1},它是掃某個 x∈L_i 的 edge (x,y) 時發現,所以有一條長度 i+1 的 path,dist(s,y)≤i+1。Y 沒出現在任何較早 layer;依 induction hypothesis,它的距離也不可能 ≤i,因此恰為 i+1。
再證「真正距離 i+1 的不會漏掉」。取 y 的 shortest path,令 x 是 y 前一個 vertex。該 prefix 也是 shortest path,所以 dist(s,x)=i,由 induction hypothesis 得 x∈L_i。掃 x 時一定檢查 (x,y);若 y 尚未 visited 就放進 L_{i+1},若已 visited,必是同一輪由另一個 L_i vertex 放入。
兩個方向合起來,layer 和最短距離完全相等。這份 proof 也指出限制:weighted graph 的 edge 代價不同,「多一條 edge」不代表「成本加一」,所以 BFS 不能直接解 weighted shortest paths。
投影片應用二:測試二分圖
Bipartite graph 能把 vertices 分成兩色,讓每條 edge 的兩端顏色不同。Slides 用兩個魚缸建模:會打架的兩條魚之間連 edge,問能否分缸避免同缸衝突。
BFS 提供線性時間測試:每個 component 各跑一次 BFS,偶數 layer 塗一色、奇數 layer 塗另一色。掃 edge 時若發現兩端同色,就回報不是 bipartite;若所有 edges 都跨色,現有 coloring 本身就是合法 certificate。
為什麼一次同色 edge 足以否定所有可能 coloring,而不只是說「這次塗得不好」?設同色 neighbors 為 u、v。它們在 BFS tree 中與共同 ancestry 的 paths 有相同 parity,再加上 edge (u,v),構成 odd cycle。Odd cycle 不可能用兩色讓相鄰 vertices 全部異色:沿 cycle 交替上色,走完奇數條 edges 回到起點時顏色衝突。因此圖不可能 bipartite。
DFS 與 BFS 的共同骨架與差異
| 面向 | DFS | BFS |
|---|---|---|
| Frontier 容器 | Stack/recursion | Queue/layers |
| 優先順序 | 最新發現的先深入 | 最早發現的先展開 |
| 主要結構 | DFS tree、discovery / finish times | BFS tree、distance layers |
| 本講應用 | Topological sorting | Unweighted shortest paths、bipartiteness |
| Adjacency-list 時間 | O(n+m) | O(n+m) |
兩者都能找 reachable vertices 與 undirected connected components。差別不在「誰比較快」,而在 traversal order 產生什麼 invariant。把 BFS 寫成 stack 或把 DFS 寫成 queue,名稱改了不算,行為與 proof 也跟著換掉。
容易誤用的限制
第一,只從一個 source 跑不代表探索全圖。Disconnected graph 需要外層 loop;directed graph 中,從 s 可到達 v 也不代表 v 可回到 s。
第二,O(n+m) 依賴 adjacency list。Matrix representation 會讓 neighbor enumeration 固定掃 n 格,dense graph 時合理,sparse graph 時則多做大量空檢查。
第三,BFS shortest path 僅限 unweighted 或每條 edge 等權。若有非負不同權重,要用 Lecture 11 的 Dijkstra;若有負權,還要換 Bellman–Ford。
第四,slides 某處把 connected graph 的 BFS 簡寫成 O(m)。完整一般式應保留 O(n+m);只有 undirected connected graph 因 m≥n-1,才能把 n 吸收進 m。
第五,topological sorting 需要 DAG;bipartite testing 要對每個 component 做 BFS。這些前提都是正確性的一部分,不是實作註腳。
這一講在整門課的位置
Lecture 9 是課程從排序與資料結構轉進 graph algorithms 的入口。它先建立 representation 與 traversal primitives,後面幾講幾乎都在改造這兩個 primitives:Lecture 10 用 DFS finish times 分解 SCC;Lecture 11 把 BFS 的無權 shortest path 推向 weighted graph;Lecture 12 再用動態規劃處理負權與 all-pairs shortest paths。
因此這一講最值得帶走的不是兩段 pseudocode,而是三個 proof habits:先說 state 代表什麼、再用每個 vertex / edge 被處理幾次做時間分析、最後把 traversal order 寫成可 induction 的 invariant。
延伸
可用同一張小圖做三次手算。第一次依不同 neighbor order 跑 DFS,比較 timestamps;第二次把 vertices 按 decreasing finish time 排出,檢查每條 DAG edge;第三次跑 BFS,逐層寫 L_i 並沿 parent 還原 shortest path。Neighbor order 會改變 tree 形狀,卻不會破壞三個核心結論。
實作 bipartite test 時,不要只回傳 boolean。遇到同色 edge 時,沿 parent pointers 找共同 ancestor,輸出那條 odd cycle。成功時回傳 coloring,失敗時回傳反例,會讓正確性從抽象宣告變成可檢查 certificate。這是本站的延伸建議,不是 Winter 2026 額外要求。
課程影片來源
已即時查核 Winter 2026 官方講次頁:每講的錄影連結都指向 Canvas/Panopto,實際打開是 Panopto 登入頁,需要 Stanford 帳號。另以搜尋查過 Stanford Online 等公開頻道,未找到同期(Winter 2026)公開錄影,因此不嵌入影片,也不以其他年份影片冒充當期錄影。
課程與錄影入口:
查核日期:2026-10-10。
更新紀錄
- 2026-10-10:標註影片狀態,核對錄影來源與取得方式。
- 2026-10-10:重查影片狀態。即時確認官方講次頁的錄影連結導向 Panopto 登入頁,查無同期公開錄影,維持原狀態。
參考資料
Loading...