9.2 最短路徑Dijkstra與Bellman-Ford#
概念說明#
帶權圖上,從起點 s 到其他每個點的最便宜路徑稱單源最短路徑。兩大演算法按「邊權能否為負」分工:
- Dijkstra:邊權非負。貪心策略——「目前距離最小的點,其距離已定案,不可能再被改善」(因為繞任何其他路都只會更貴)。用最小堆積挑出定案點,效率 O((V+E) log V)。
- Bellman-Ford:允許負權邊。對所有邊做 V−1 輪「放鬆」(relaxation),每輪至少讓一個點的距離達到最終值;之後再做一輪,若仍有邊可放鬆,代表存在負環(最短路無定義)。
「放鬆」是共通核心操作:若 dist[u] + w(u,v) < dist[v],就更新 dist[v] 並記下前驅。
操作步驟#
Dijkstra: 1. dist[s] = 0,其餘 ∞,全部丟進(或逐步丟進)最小堆積。 2. 取出距離最小的點 u(已定案)。 3. 對 u 的每個鄰居 v 放鬆;成功則把 (新距離, v) push 進堆積。 4. 重複到堆積空。
Bellman-Ford: 1. dist[s] = 0,其餘 ∞。 2. 重複 V−1 輪:對每條邊 (u, v, w) 嘗試放鬆。 3. 第 V 輪檢查:若仍有邊可放鬆 → 有負環。
Python 實作#
import heapq
def dijkstra(graph, start): # graph: {u: [(v, w), ...]}
dist = {start: 0}
pq = [(0, start)] # (距離, 節點)
while pq:
d, u = heapq.heappop(pq)
if d > dist.get(u, float("inf")):
continue # 過期條目,跳過(惰性刪除)
for v, w in graph.get(u, []):
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist
def dijkstra_path(graph, start, end):
dist = {start: 0}
prev = {}
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq)
if u == end:
break
if d > dist.get(u, float("inf")):
continue
for v, w in graph.get(u, []):
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd
prev[v] = u
heapq.heappush(pq, (nd, v))
if end not in dist:
return None, float("inf")
path, cur = [], end
while cur != start:
path.append(cur)
cur = prev[cur]
return [start] + path[::-1], dist[end]
def bellman_ford(n, edges, start): # edges: [(u, v, w), ...]
dist = [float("inf")] * n
dist[start] = 0
for _ in range(n - 1):
changed = False
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
changed = True
if not changed: # 提前結束
break
for u, v, w in edges: # 負環檢查
if dist[u] + w < dist[v]:
return dist, True # 有負環
return dist, False
# 城市路網(里程)
roads = {
"台北": [("台中", 160), ("花蓮", 130)],
"台中": [("台北", 160), ("嘉義", 90)],
"花蓮": [("台北", 130), ("台東", 190)],
"嘉義": [("台中", 90), ("台南", 60)],
"台東": [("花蓮", 190)],
}
d = dijkstra(roads, "台北")
print(d) # 台北→台中→嘉義 = 250;台北→花蓮→台東 = 320
print(dijkstra_path(roads, "台北", "台東")) # (['台北', '花蓮', '台東'], 320)
# 含負權:優惠券讓某條路變成 -50
n = 5
edges = [(0, 1, 4), (0, 2, 1), (2, 1, -2), (1, 3, 5), (2, 3, 8)]
dist, neg = bellman_ford(n, edges, 0)
print(dist, "負環:", neg) # [0, -1, 1, 4] False
複雜度分析#
| 演算法 | 時間 | 負權邊 | 用途 |
|---|---|---|---|
| Dijkstra(堆積) | O((V+E) log V) | ✗ | 路網導航、圖論主力 |
| Bellman-Ford | O(V·E) | ✓ | 負權、負環偵測、分布式路由 |
| BFS(無權圖) | O(V+E) | — | 等權最短路,最快 |
練習#
- 在
dijkstra_path的基礎上實作「多點直達」:回傳 start 到所有點的最短路徑樹(每點的路徑與距離)。 - 用 Bellman-Ford 建構匯率套利模型:把各貨幣當節點、匯率取 log 後取負當邊權,正的套利機會會以什麼形式被偵測到?
- 當圖是「所有邊權都是 1」時,BFS 比 Dijkstra 快且不用堆積。寫一個通用函式:邊權全等於 1 時自動改用 BFS,否則用 Dijkstra。