Bellman-Ford 與 Floyd-Warshall
邊的權重可以是負的時候,Dijkstra 會算錯:Bellman-Ford 反覆放鬆每一條邊,還能偵測負環;Floyd-Warshall 一次算出所有點對之間的最短路徑。
做法
圖
1/17
剛變短正在檢查的邊已經走得到(小圓點是目前距離)
| 點 | S | A | B | C | D |
|---|---|---|---|---|---|
| Bellman-Ford 目前 | 0 | ∞ | ∞ | ∞ | ∞ |
| Dijkstra 的答案 | 0 | 1 | 5 | 3 | 4 |
Dijkstra 一開始就把 A 定在 1(當時最小的距離),之後再也不回頭看 A。它不會知道 S → B → A 只要 5 − 10 = −5,A 後面的點也跟著錯。
開始:S 的距離是 0,其他都是 ∞。接著把每條邊照固定順序「放鬆」一遍,最多做 4 輪(V − 1 輪)。
第幾輪
0 / 4
檢查邊的次數
0
負環
…
亮起來的是這一步執行的程式碼
type Edge = [number, number, number]; // [u, v, weight] function bellmanFord(n: number, edges: Edge[], source: number): { dist: number[]; negativeCycle: boolean } { const dist = new Array(n).fill(Infinity); dist[source] = 0; for (let round = 1; round < n; round++) { let changed = false; for (const [u, v, w] of edges) { if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; changed = true; } } if (!changed) return { dist, negativeCycle: false }; } // Still improving after n − 1 rounds: a negative cycle. for (const [u, v, w] of edges) { if (dist[u] + w < dist[v]) return { dist, negativeCycle: true }; } return { dist, negativeCycle: false };} function floydWarshall(n: number, edges: Edge[]): number[][] { const d = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity))); for (const [u, v, w] of edges) d[u][v] = Math.min(d[u][v], w); for (let k = 0; k < n; k++) for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) if (d[i][k] + d[k][j] < d[i][j]) d[i][j] = d[i][k] + d[k][j]; return d; // a negative d[i][i] means a negative cycle through i}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- Bellman-Ford:邊的權重可能是負的(退款、匯率的對數、時間差),或需要知道有沒有負環。
- Floyd-Warshall:點不多(幾百個以內)、又要任兩點之間的距離,尤其圖很稠密時;程式短到不容易寫錯。
- 權重全是正的,就用 Dijkstra:快得多。
和其他主題的關係
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| Bellman-Ford,一個起點 最多 V − 1 輪,每輪看每條邊;提早結束時常常少很多 | O(VE) | O(VE) |
| Floyd-Warshall,所有點對 三層迴圈,和邊數無關 | O(V³) | O(V³) |
| 對照:Dijkstra(二元堆積) 快,但不能有負權重 | O(E log V) | O(E log V) |
空間:O(V) / O(V²),Bellman-Ford 一個距離陣列;Floyd-Warshall 一整張 V × V 表
Big O 實測:n 變大時步數怎麼長
數的是:檢查邊的次數(一條路、邊倒著排,最差情況)
| Big O | n = 100 | n = 200 | n = 400 | n = 800 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| Bellman-Ford | O(n²) | 9,900 | 39,800 | 159,600 | 639,200 | ×65 (×64) |
邊倒著排時,每一輪只能往前推進一個點,所以要整整 V − 1 輪,每輪看 V − 1 條邊:V 和 E 一起長,O(VE) 就是 O(n²)。Floyd-Warshall 的 V³ 見上方表格。
和其他做法比
| 邊數 E | Bellman-Ford 一個起點:檢查邊 | 實際輪數 | Bellman-Ford × 每個起點 | Floyd-Warshall:V³ | |
|---|---|---|---|---|---|
| 50 個點,稀疏 | 147 | 1,029 | 7 | 46,599 | 125,000 |
| 50 個點,稠密 | 1,552 | 6,208 | 4 | 301,088 | 125,000 |
| 100 個點,稀疏 | 287 | 1,722 | 6 | 201,474 | 1,000,000 |
| 100 個點,稠密 | 6,291 | 18,873 | 3 | 2,579,310 | 1,000,000 |
隨機圖、權重 1 到 20,次數是實際跑出來的。Bellman-Ford 有提早結束,所以在隨機圖上通常幾輪就穩定,遠少於 V − 1 輪。要所有點對之間的距離時:稀疏圖從每個點各跑一次 Bellman-Ford 比較省;邊一多,Floyd-Warshall 固定的 V³ 反而比較少,而且程式只有三層迴圈。
真實世界裡的它
- 距離向量路由協定(例如 RIP,Routing Information Protocol):每台路由器只跟鄰居交換距離,本質上就是分散式的 Bellman-Ford。
- 找套利機會:把匯率取負對數當權重,負環就代表繞一圈換回來會變多。
- Floyd-Warshall 的同一個三層迴圈,把「加、取最小」換成「且、或」,就是求遞移閉包(誰能走到誰)。
取捨與陷阱
- 在有負權重的圖上用 Dijkstra:它不會報錯,只會安靜地給出錯的答案(見上方示範)。
- ∞ 加上負數還是 ∞:拿浮點數的 Infinity 沒問題,但若用很大的整數代表 ∞,要先檢查 dist[u] 不是 ∞ 再相加,否則會算出「∞ − 6」這種假距離,或整數溢位。
- Bellman-Ford 只看得到從起點走得到的負環;要找全圖的負環,先加一個連到所有點、權重 0 的虛擬起點。
- Floyd-Warshall 的 k 一定要在最外層;把迴圈順序換掉,程式照樣跑得完,答案卻是錯的。
LeetCode 練習
- 743.Network Delay TimeMedium單一起點最短路徑,可以用 Bellman-Ford 對照 Dijkstra(在新分頁開啟 LeetCode)
- 787.Cheapest Flights Within K StopsMediumBellman-Ford 只放鬆 k + 1 輪,剛好限制轉機次數(在新分頁開啟 LeetCode)
- 1334.Find the City With the Smallest Number of Neighbors at a Threshold DistanceMediumFloyd-Warshall 算出所有點對的距離(在新分頁開啟 LeetCode)