1959 - Dijkstra 的最短路徑演算法(演算法設計的典範)#
案件摘要#
1959 年,Edsger W. Dijkstra(1930–2002) 在《數學年鑑》發表 A Note on Two Problems in Connexion with Graphs:
最短路徑演算法(Dijkstra's algorithm):
時間複雜度:
Dijkstra 1959 年在咖啡廳的靈感:他自己說:
「我和未婚妻在阿姆斯特丹的咖啡廳,20 分鐘內我設計出了這個演算法—— 沒有紙筆。」 這個演算法成為地圖導航(Google Maps)、網路路由(OSPF)、 排程問題的基礎——1972 年 Dijkstra 獲得圖靈獎。
前因 -- 為什麼會有這個案子#
- Dijkstra 的背景(1950s): Dijkstra 是荷蘭的程式設計師,在 Mathematisch Centrum(阿姆斯特丹數學中心) 負責 ARMAC 電腦的軟體。 1956 年,他需要一個演示程式來展示 ARMAC 的能力—— 他選了「從荷蘭的 64 個城市中,找出兩城之間的最短路徑」。
- 圖論的基礎(1736–1950s):
- Euler(1736):七橋問題——圖論的誕生;
- Prim(1957)、Kruskal(1956):最小生成樹;
- Bellman(1958):動態規劃的最短路徑(Bellman–Ford 演算法,可處理負邊)。
- Dijkstra 的洞察(1956/1959): 貪心策略:每次選「最近的未訪問點」——
這個貪心策略的正確性需要證明(不變量法)。
線索與推理 -- 數學式、程式、理論#
第一條線索:Dijkstra 演算法的形式描述#
演算法: 1. 初始化: \(d(s) = 0\) ,其他 \(d(v) = \infty\) ;已訪問集合 \(S = \emptyset\) ; 2. 選取未訪問點中 \(d\) 最小的 \(u\) ,加入 \(S\) ; 3. 對 \(u\) 的每個鄰居 \(v\) :
- 重複 2–3 直到所有點已訪問。
第二條線索:正確性的證明(不變量法)#
不變量(loop invariant):
證明(反證法): 假設 \(d(u)\) 不是最短——存在更短的路徑 \(P\) 從 \(s\) 到 \(u\) 。 \(P\) 必須經過某個「未訪問」的點 \(x\) (離開 \(S\) 的第一步):
但 Dijkstra 每次選 \(d\) 最小的——故 \(d(x) \ge d(u)\) ,矛盾!
第三條線索:複雜度的改進(1959–1984)#
| 實現 | 時間複雜度 | 年份 |
|---|---|---|
| 陣列 | \(O(V^2)\) | 1959(Dijkstra 原版) |
| 二元堆積 | \(O((V+E)\log V)\) | 1964(Williams 的 heap) |
| Fibonacci 堆積 | \(O(E + V\log V)\) | 1984(Fredman–Tarjan,1986 圖靈獎 Tarjan) |
A* 演算法(1968,Hart–Nilsson–Raphael):
加入啟發式(如直線距離)後,只探索「有希望」的路徑—— 這是 Google Maps 的核心(實際上用 Contraction Hierarchies,2010s)。
Python:Dijkstra 演算法與 A* 的實現#
import heapq
# 1) Dijkstra 演算法(二元堆積實現,O((V+E) log V))
def dijkstra(graph, start):
"""graph: {node: [(neighbor, weight), ...]}"""
dist = {node: float('inf') for node in graph}
dist[start] = 0
prev = {node: None for node in graph}
visited = set()
pq = [(0, start)] # (距離, 節點)
while pq:
d, u = heapq.heappop(pq)
if u in visited:
continue
visited.add(u)
for v, w in graph[u]:
if d + w < dist[v]: # 鬆弛
dist[v] = d + w
prev[v] = u
heapq.heappush(pq, (dist[v], v))
return dist, prev
def shortest_path(prev, target):
"""回溯最短路徑"""
path = []
while target is not None:
path.append(target)
target = prev[target]
return path[::-1]
# 2) 測試:荷蘭城市的地圖(Dijkstra 1956 的原始問題)
graph = {
'阿姆斯特丹': [('烏特勒支', 4), ('海牙', 6)],
'烏特勒支': [('阿姆斯特丹', 4), ('鹿特丹', 3), ('恩荷芬', 8)],
'海牙': [('阿姆斯特丹', 6), ('鹿特丹', 2)],
'鹿特丹': [('烏特勒支', 3), ('海牙', 2), ('恩荷芬', 5)],
'恩荷芬': [('烏特勒支', 8), ('鹿特丹', 5)],
}
print("Dijkstra 最短路徑(荷蘭城市):")
dist, prev = dijkstra(graph, '阿姆斯特丹')
for city in ['海牙', '鹿特丹', '恩荷芬', '烏特勒支']:
path = shortest_path(prev, city)
print(f" 阿姆斯特丹 → {city}: 距離 = {dist[city]}, 路徑 = {' → '.join(path)}")
# 3) 複雜度驗證:邊的鬆弛次數
print("\n鬆弛操作的次數(O(E + V log V) 的驗證):")
n_relax = 0
def dijkstra_count(graph, start):
global n_relax
n_relax = 0
dist = {node: float('inf') for node in graph}
dist[start] = 0
visited = set()
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq)
if u in visited: continue
visited.add(u)
for v, w in graph[u]:
n_relax += 1
if d + w < dist[v]:
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))
return dist
E = sum(len(adj) for adj in graph.values())
V = len(graph)
dijkstra_count(graph, '阿姆斯特丹')
print(f" V = {V}, E = {E}")
print(f" 鬆弛次數 = {n_relax}(≤ E = {E})")
print(" → 鬆弛次數 = O(E),堆積操作 = O(V log V)")
# 4) A* 演算法:加入啟發式(Dijkstra 1968 的改進)
print("\nA* 演算法(Dijkstra + 啟發式):")
def astar(graph, start, goal, h):
"""h: 啟發式函數(如直線距離)"""
dist = {start: 0}
prev = {start: None}
pq = [(h(start, goal), 0, start)] # (f = g + h, g, 節點)
visited = set()
explored = 0
while pq:
f, g, u = heapq.heappop(pq)
if u in visited: continue
visited.add(u)
explored += 1
if u == goal:
return dist[goal], prev, explored
for v, w in graph[u]:
if g + w < dist.get(v, float('inf')):
dist[v] = g + w
prev[v] = u
heapq.heappush(pq, (g + w + h(v, goal), g + w, v))
return None, prev, explored
# 啟發式:城市間的「座標距離」(簡化用字母序距離)
coords = {'阿姆斯特丹': (0, 0), '烏特勒支': (4, 0), '海牙': (0, 6),
'鹿特丹': (3, 6), '恩荷芬': (8, 2)}
def h(a, b):
(x1,y1), (x2,y2) = coords[a], coords[b]
return ((x1-x2)**2 + (y1-y2)**2)**0.5
d, prev, explored = astar(graph, '阿姆斯特丹', '恩荷芬', h)
path = shortest_path(prev, '恩荷芬')
print(f" 阿姆斯特丹 → 恩荷芬: 距離 = {d}, 路徑 = {' → '.join(path)}")
print(f" A* 探索了 {explored} 個節點")
d2, _ = dijkstra(graph, '阿姆斯特丹')
print(f" Dijkstra 需探索全部 {len(graph)} 個節點")
print(" → A* 只探索「有希望」的路徑(啟發式的威力)")
Dijkstra 最短路徑(荷蘭城市):
阿姆斯特丹 → 海牙: 距離 = 6, 路徑 = 阿姆斯特丹 → 海牙
阿姆斯特丹 → 鹿特丹: 距離 = 7, 路徑 = 阿姆斯特丹 → 烏特勒支 → 鹿特丹
阿姆斯特丹 → 恩荷芬: 距離 = 12, 路徑 = 阿姆斯特丹 → 烏特勒支 → 恩荷芬
阿姆斯特丹 → 烏特勒支: 距離 = 4, 路徑 = 阿姆斯特丹 → 烏特勒支
鬆弛操作的次數(O(E + V log V) 的驗證):
V = 5, E = 12
鬆弛次數 = 12(≤ E = 12)
→ 鬆弛次數 = O(E),堆積操作 = O(V log V)
A* 演算法(Dijkstra + 啟發式):
阿姆斯特丹 → 恩荷芬: 距離 = 12, 路徑 = 阿姆斯特丹 → 烏特勒支 → 恩荷芬
A* 探索了 3 個節點
Dijkstra 需探索全部 5 個節點
→ A* 只探索「有希望」的路徑(啟發式的威力)
結案 -- 後果與影響#
- 演算法設計的典範(1959):
Dijkstra 的演算法樹立了「演算法 = 正確性證明 + 複雜度分析」的典範—— Knuth(1974 圖靈獎)的《演算法藝術》把這個典範發揚光大。 - 地圖導航的基礎(1959–):
每天數十億次的路徑查詢都是 Dijkstra 的後代—— 這是資訊科學中「被使用次數最多」的演算法之一。 - 網路路由(1970s–): - OSPF(開放最短路徑優先,1989):網際網路的路由協定—— 就是 Dijkstra 演算法; - IS-IS(1987):電信網路的路由。
- Dijkstra 的其他發明(1959–1968):
- 信號量(semaphore,1965):並行程式的同步原語—— 作業系統的基礎(見
作業系統/系列的相關案例); - 結構化程式設計(1968):《GOTO 有害論》—— 程式設計的革命(見「1968-結構化程式設計.md」);
- THE 作業系統(1968):第一個分層設計的 OS。
- 1972 年圖靈獎的演講: Dijkstra 的演講題為《謙遜的程式設計師》(The Humble Programmer)—— 他說:
「程式設計的挑戰不是把機器塞進程式,而是把我們的頭腦塞進程式。」
- Fibonacci 堆積(1984): Fredman 與 Tarjan(Tarjan 是 1986 圖靈獎得主)的 Fibonacci 堆積—— **把 Dijkstra 的複雜度從 \(O((V+E)\log V)\) 降到 \(O(E + V\log V)\) **—— 理論的勝利(但實務上二元堆積更快)。
- 現代應用(2000s–):
- Google Maps / 百度地圖:路徑規劃;
- 物流與運輸:快遞路徑(UPS 的 ORION 系統每年省 10 億英里);
- 遊戲:NPC 的尋路(A*);
- 機器人:路徑規劃(RRT、D* Lite)。
- 歷史定位:Dijkstra 是「演算法設計」的奠基者——
「最簡單的演算法,最深刻的思想」。
關鍵人物與文獻#
- E. W. Dijkstra:A Note on Two Problems in Connexion with Graphs(1959);信號量(1965);《GOTO 有害論》(1968);1972 圖靈獎。
- R. Bellman:動態規劃(1958)——Bellman–Ford 演算法。
- R. Prim(1957)與 J. Kruskal(1956):最小生成樹。
- P. E. Hart, N. J. Nilsson, B. Raphael:A* 演算法(1968)。
- M. Fredman & R. Tarjan:Fibonacci 堆積(1984);Tarjan 1986 圖靈獎。
- 相關案件:
1968-結構化程式設計.md、1968-克努特演算法藝術.md、1971-庫克NP完全.md。