跳到主要內容

演算法

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

主題 · Bellman-Ford 與 Floyd-Warshall

Bellman-Ford 與 Floyd-Warshall

邊的權重可以是負的時候,Dijkstra 會算錯:Bellman-Ford 反覆放鬆每一條邊,還能偵測負環;Floyd-Warshall 一次算出所有點對之間的最短路徑。

做法
圖
1/17
15-102110S0A∞B∞C∞D∞
剛變短正在檢查的邊已經走得到(小圓點是目前距離)
點SABCD
Bellman-Ford 目前0∞∞∞∞
Dijkstra 的答案01534

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 On = 100n = 200n = 400n = 800成長倍數:實測(理論)
Bellman-FordO(n²)9,90039,800159,600639,200×65 (×64)

邊倒著排時,每一輪只能往前推進一個點,所以要整整 V − 1 輪,每輪看 V − 1 條邊:V 和 E 一起長,O(VE) 就是 O(n²)。Floyd-Warshall 的 V³ 見上方表格。

和其他做法比

邊數 EBellman-Ford 一個起點:檢查邊實際輪數Bellman-Ford × 每個起點Floyd-Warshall:V³
50 個點,稀疏1471,029746,599125,000
50 個點,稠密1,5526,2084301,088125,000
100 個點,稀疏2871,7226201,4741,000,000
100 個點,稠密6,29118,87332,579,3101,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 練習