cover

Google Maps 怎麼算出最短路線?Dijkstra + A*。網路路由器怎麼決定封包往哪走?Dijkstra。金融系統怎麼偵測套利機會?Bellman-Ford 找負環。圖演算法不是學術題目,它藏在你每天用的工具裡。

先講結論

你想做什麼用什麼時間複雜度
單源最短路徑(無負權)DijkstraO((V+E) log V)
單源最短路徑(有負權)Bellman-FordO(V×E)
所有點對最短路徑Floyd-WarshallO(V³)
點對點(有啟發函數)A*O(E) 最佳

沒有負權重 → Dijkstra。有負權重 → Bellman-Ford。要算所有配對 → Floyd-Warshall。地圖導航 → A*。就這麼簡單。

Dijkstra:貪心找最短路

核心思想:每次從「還沒確定」的節點中,挑距離最小的。一旦挑中,它的最短距離就確定了(因為沒有負權重,後面不可能更短)。

從節點 1 出發:
1. dist = [0, ∞, ∞, ∞]
2. 更新 1 的鄰居:dist = [0, 4, 2, ∞]
3. 選最小的 3(dist=2)→ 確定
4. 更新 3 的鄰居:dist = [0, 4, 2, 5]
5. 選最小的 2(dist=4)→ 確定
6. 最後 4(dist=5)→ 確定

用 Priority Queue 實作:

int[] dijkstra(Graph g, int source) {
    int[] dist = new int[V];
    Arrays.fill(dist, INF);
    dist[source] = 0;
 
    PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0]-b[0]);
    pq.offer(new int[]{0, source});
 
    while (!pq.isEmpty()) {
        int[] curr = pq.poll();
        int d = curr[0], u = curr[1];
        if (d > dist[u]) continue;  // 已有更短路徑
 
        for (Edge e : neighbors(u)) {
            int newDist = dist[u] + e.weight;
            if (newDist < dist[e.to]) {
                dist[e.to] = newDist;
                pq.offer(new int[]{newDist, e.to});
            }
        }
    }
    return dist;
}

致命限制:不能有負權重邊。 為什麼?因為 Dijkstra 的貪心假設是「確定的節點不會再被更新」。負權重會打破這個假設。

Bellman-Ford:暴力但萬用

對所有邊做 V-1 次鬆弛。暴力?是的。但它能處理負權重,還能偵測負環。

for (int i = 0; i < V - 1; i++) {
    for (每條邊 (u, v, w)) {
        if (dist[u] + w < dist[v]) {
            dist[v] = dist[u] + w;
        }
    }
}
 
// 第 V 次還能更新 = 有負環
for (每條邊 (u, v, w)) {
    if (dist[u] + w < dist[v]) return "有負環!";
}

工程場景:外匯市場的套利偵測。把幣別當節點、匯率取 log 當邊權重,如果存在負環,就表示有套利機會(換一圈回來錢變多了)。

Floyd-Warshall:所有配對

三層迴圈,DP 思維:「從 i 到 j,允許經過節點 0~k 的最短路徑是多少?」

for (int k = 0; k < V; k++)
    for (int i = 0; i < V; i++)
        for (int j = 0; j < V; j++)
            dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);

O(V³) 聽起來很慢,但如果你需要所有配對的最短路徑,它是最簡單的選擇。而且只有三行核心程式碼,debug 的時候非常舒服。

A*:比 Dijkstra 更聰明

Dijkstra 像無頭蒼蠅,往所有方向擴展。A* 多了一個啟發函數 h(n),估計從 n 到終點的距離,讓搜尋方向朝目標前進。

f(n) = g(n) + h(n)
g(n) = 起點到 n 的實際距離
h(n) = n 到終點的估計距離

常用的啟發函數:

  • 曼哈頓距離(|x1-x2| + |y1-y2|):網格地圖,只能上下左右
  • 歐幾里得距離:任意方向移動
  • 切比雪夫距離(max(|Δx|, |Δy|)):含對角線移動

Google Maps 用的就是 A* 的變體。地圖資料是一張巨大的加權圖(路口=節點、道路=邊),用直線距離當啟發函數,避免向反方向搜尋。實際的生產系統會再加上 Contraction Hierarchies 等預處理,讓查詢從數秒降到毫秒。

拓撲排序和其他進階圖論主題請看 圖演算法(下):拓撲排序與工程應用


Dijkstra 在 1956 年用 20 分鐘想出了這個演算法。我用了 20 分鐘理解它,然後又用了 2 小時 debug 我的 Priority Queue 實作。