最短路徑演算法 — Dijkstra、Bellman-Ford、Floyd-Warshall 完整比較 | 資料結構與演算法

2026/07/10
最短路徑演算法 — Dijkstra、Bellman-Ford、Floyd-Warshall 完整比較 | 資料結構與演算法

最短路徑演算法(Shortest Path Algorithm) 是圖論中最核心的問題之一。從 GPS 導航選擇最快路線,到網路封包選擇最短跳數路由,背後都是這一類演算法在運作。本文系統介紹三大主流演算法:Dijkstra(單源正權)、Bellman-Ford(支援負權、可偵測負環)、Floyd-Warshall(全源最短路),並涵蓋 A* 啟發式搜尋與 SPFA 優化。

前言

想像你正在用 Google Maps 規劃從台北到高雄的行車路線。地圖上有數千個路口(節點)、數萬條道路(邊),每條道路有不同的行車時間(邊權重)。你的目標是找出總時間最短的那條路徑

這就是「最短路徑問題(Shortest Path Problem)」的本質——在加權圖中,找到起點到終點之間,邊權重總和最小的路徑。

問題依規模分為兩大類:

  • 單源最短路徑(Single-Source Shortest Path,SSSP):從一個起點出發,求到所有其他節點的最短距離
  • 全源最短路徑(All-Pairs Shortest Path,APSP):求圖中所有節點對之間的最短距離

讀完本文,你將能夠:

  • 理解鬆弛操作(Relaxation) 這個貫穿所有最短路徑演算法的核心概念
  • 掌握 Dijkstra 的貪心策略與優先佇列(Min-Heap)實作
  • 理解 Bellman-Ford 如何處理負權邊並偵測負環(Negative Cycle)
  • 學會 Floyd-Warshall 的動態規劃三層迴圈寫法
  • 了解 A* 啟發式搜尋和 SPFA 優化的應用場景
  • 解析 5 道 LeetCode 精選題(743、787、1631、778、1514)

核心概念

鬆弛操作(Relaxation)

所有最短路徑演算法的核心都是「鬆弛操作」——對一條邊 (u, v, w) 進行鬆弛,意思是:如果透過 u 繞路比目前已知的 dist[v] 更短,就更新它。

鬆弛操作定義:
  if dist[u] + w(u, v) < dist[v]:
      dist[v] = dist[u] + w(u, v)   ← 更新!找到更短路徑

初始狀態:
  dist[起點] = 0
  dist[其他節點] = Infinity(尚未發現路徑)

每次成功鬆弛,dist[v] 就變小一點,最終收斂到真正的最短距離。

三大演算法的差異在於:以什麼順序、鬆弛多少次


Dijkstra 演算法

Dijkstra 採用貪心策略:每次從已知節點中取出距離起點最近的那個,再用它去鬆弛它的鄰居。這需要一個最小優先佇列(Min-Heap) 支撐。

限制:只適用於非負權邊。因為貪心前提是「已確定最短距離的節點不會再被更新」,負權邊會打破這個假設。

圖結構:A→B(4) A→C(2) B→D(3) B→E(1) C→B(1) C→D(5) D→E(2)

        A ──4── B ──3── D
        │       │       │
        2       1       2
        │       │       │
        C ──1── ┘       E
        │               ↑
        └───────5───────┘

初始化(起點 A):
  dist = { A:0, B:∞, C:∞, D:∞, E:∞ }
  Min-Heap = [(0,A)]

步驟 1:取出 (0,A),鬆弛 A 的鄰居
  A→B: dist[B] = min(∞, 0+4) = 4
  A→C: dist[C] = min(∞, 0+2) = 2
  Heap = [(2,C), (4,B)]

步驟 2:取出 (2,C),鬆弛 C 的鄰居
  C→B: dist[B] = min(4, 2+1) = 3  ← 更新!
  C→D: dist[D] = min(∞, 2+5) = 7
  Heap = [(3,B), (4,B_stale), (7,D)]

步驟 3:取出 (3,B),鬆弛 B 的鄰居
  B→D: dist[D] = min(7, 3+3) = 6  ← 更新!
  B→E: dist[E] = min(∞, 3+1) = 4
  Heap = [(4,B_stale), (4,E), (6,D), (7,D_stale)]

步驟 4:取出 (4,B_stale),dist[B]=3 < 4,Lazy Deletion 跳過
步驟 5:取出 (4,E),E 無出邊
步驟 6:取出 (6,D),D→E: min(4, 6+2) = 4,不更新

最終結果:{ A:0, B:3, C:2, D:6, E:4 }

Lazy Deletion:堆積中可能有過時的 (舊距離, 節點) 項目。取出時若 d > dist[u],直接跳過即可,無需從堆積中主動刪除。


Bellman-Ford 演算法

Bellman-Ford 採用動態規劃策略:對所有邊進行 V-1 次鬆弛操作(V 為頂點數)。原理:最短路徑最多包含 V-1 條邊,所以 V-1 輪鬆弛足以讓所有距離收斂。

優點

  • 支援負權邊
  • 第 V 次鬆弛若仍有更新 → 存在負環(Negative Cycle)

缺點:時間複雜度 O(VE),比 Dijkstra 慢。

負環範例:
  A →(1)→ B →(1)→ C →(-5)→ A

  每走一圈這個環,總距離下降 3。
  如果最短路徑必須經過此環,距離會趨近 -∞。
  → 最短路徑不存在!必須偵測並報告。

Bellman-Ford 偵測負環:
  執行完 V-1 次鬆弛後,再做第 V 次鬆弛。
  如果第 V 次仍有 dist 更新 → 存在負環。

Floyd-Warshall 演算法(全源最短路)

Floyd-Warshall 用三層迴圈的動態規劃求所有節點對的最短路。

核心思路:以每個節點 k 作為中繼點,嘗試「能否透過 k 讓 i→j 的路徑更短?」

狀態:dp[i][j] = 節點 i 到節點 j 的最短距離

轉移(k 從 0 到 V-1):
  dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])

      i ────────────────── j     (直接路徑)
      i ────── k ────────  j     (經由 k 中轉)

注意:k 必須在最外層迴圈!
  錯誤:for i / for j / for k
  正確:for k / for i / for j

負環偵測:若最終 dp[i][i] < 0,則存在負環。

適用場景:V 較小(≤ 500),需要任意兩點間距離的查詢。


SPFA(Bellman-Ford 的佇列優化)

SPFA(Shortest Path Faster Algorithm)是 Bellman-Ford 的佇列優化版本:只把「距離被更新過的節點」加入佇列,避免對所有邊做無效鬆弛。

  • 平均時間複雜度:O(kE),k 接近常數(通常很快)
  • 最壞時間複雜度:O(VE)(退化為 Bellman-Ford)
  • 在競技程式設計中常用,但實際表現不穩定,不推薦用於正式場景

A*(A-Star)啟發式搜尋

A* 是 Dijkstra 的啟發式延伸。在 Dijkstra 的排序依據 f = g(已知實際距離)的基礎上,加上啟發函數 h(估算到終點的距離):

f(n) = g(n) + h(n)
  g(n) = 從起點到節點 n 的實際最短距離
  h(n) = 從節點 n 到終點的「估算」距離(啟發函數)

優先佇列以 f 值排序(小的先取出)。

啟發函數要求:h(n) 必須是「可接受的」(admissible),
  即 h(n) ≤ 真實距離,否則無法保證最優解。

常見啟發函數:
  曼哈頓距離(Manhattan):|dr| + |dc],適合網格(禁止對角移動)
  歐式距離(Euclidean):sqrt(dr² + dc²),適合任意方向

JS/TS 實作

Dijkstra(MinHeap 版)

完整實作,包含自訂 MinHeap 確保 O(log n) 的堆積操作:

type Edge = [number, number]; // [neighbor, weight]
type Graph = Map<number, Edge[]>;

class MinHeap {
  private heap: [number, number][] = []; // [dist, node]

  push(item: [number, number]): void {
    this.heap.push(item);
    this.siftUp(this.heap.length - 1);
  }

  pop(): [number, number] | undefined {
    if (this.heap.length === 0) return undefined;
    const top = this.heap[0];
    const last = this.heap.pop()!;
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.siftDown(0);
    }
    return top;
  }

  get size(): number {
    return this.heap.length;
  }

  private siftUp(i: number): void {
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (this.heap[parent][0] > this.heap[i][0]) {
        [this.heap[parent], this.heap[i]] = [this.heap[i], this.heap[parent]];
        i = parent;
      } else break;
    }
  }

  private siftDown(i: number): void {
    const n = this.heap.length;
    while (true) {
      let smallest = i;
      const left = 2 * i + 1;
      const right = 2 * i + 2;
      if (left < n && this.heap[left][0] < this.heap[smallest][0]) smallest = left;
      if (right < n && this.heap[right][0] < this.heap[smallest][0]) smallest = right;
      if (smallest === i) break;
      [this.heap[smallest], this.heap[i]] = [this.heap[i], this.heap[smallest]];
      i = smallest;
    }
  }
}

function dijkstra(graph: Graph, n: number, src: number): number[] {
  const dist = new Array(n + 1).fill(Infinity);
  dist[src] = 0;
  const heap = new MinHeap();
  heap.push([0, src]);

  while (heap.size > 0) {
    const [d, u] = heap.pop()!;
    // Lazy Deletion:若已找到更短路徑則跳過
    if (d > dist[u]) continue;

    for (const [v, w] of (graph.get(u) ?? [])) {
      const newDist = dist[u] + w;
      if (newDist < dist[v]) {
        dist[v] = newDist;
        heap.push([newDist, v]);
      }
    }
  }

  return dist;
}

Bellman-Ford(含負環偵測)

interface WeightedEdge {
  from: number;
  to: number;
  weight: number;
}

interface BellmanFordResult {
  dist: number[];
  hasNegativeCycle: boolean;
}

function bellmanFord(
  edges: WeightedEdge[],
  n: number,
  src: number
): BellmanFordResult {
  const dist = new Array(n + 1).fill(Infinity);
  dist[src] = 0;

  // 鬆弛 V-1 次,保證最短路徑最多含 V-1 條邊
  for (let i = 0; i < n - 1; i++) {
    let updated = false;
    for (const { from, to, weight } of edges) {
      if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
        dist[to] = dist[from] + weight;
        updated = true;
      }
    }
    // 提前終止優化:若無更新則已收斂
    if (!updated) break;
  }

  // 第 V 次鬆弛若仍有更新 → 存在負環
  let hasNegativeCycle = false;
  for (const { from, to, weight } of edges) {
    if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
      hasNegativeCycle = true;
      break;
    }
  }

  return { dist, hasNegativeCycle };
}

Floyd-Warshall(全源最短路)

function floydWarshall(n: number, edges: [number, number, number][]): number[][] {
  // dist[i][j] = 節點 i 到節點 j 的最短距離
  const INF = Infinity;
  const dist: number[][] = Array.from({ length: n }, (_, i) =>
    Array.from({ length: n }, (_, j) => (i === j ? 0 : INF))
  );

  // 初始化直接邊
  for (const [u, v, w] of edges) {
    dist[u][v] = Math.min(dist[u][v], w);
  }

  // 中繼節點 k:嘗試「經由 k 中轉」是否能縮短 i→j 路徑
  // 注意:k 必須在最外層!
  for (let k = 0; k < n; k++) {
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        if (dist[i][k] !== INF && dist[k][j] !== INF) {
          dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
        }
      }
    }
  }

  // 負環偵測:若 dist[i][i] < 0,則存在負環
  for (let i = 0; i < n; i++) {
    if (dist[i][i] < 0) {
      throw new Error(`Negative cycle detected involving node ${i}`);
    }
  }

  return dist;
}

A*(Manhattan Distance 啟發函數,網格應用)

interface Point {
  row: number;
  col: number;
}

// Manhattan Distance:適用於網格,禁止對角移動時的最優啟發函數
function manhattan(a: Point, b: Point): number {
  return Math.abs(a.row - b.row) + Math.abs(a.col - b.col);
}

function aStar(
  grid: number[][],
  start: Point,
  end: Point
): number {
  const rows = grid.length;
  const cols = grid[0].length;
  const dirs = [[0,1],[0,-1],[1,0],[-1,0]];

  // g[r][c] = 從起點到 (r,c) 的實際最短距離
  const g: number[][] = Array.from({ length: rows }, () =>
    new Array(cols).fill(Infinity)
  );
  g[start.row][start.col] = 0;

  // f = g + h,Min-Heap 以 f 值排序 [f, gCost, row, col]
  const heap: [number, number, number, number][] = [];
  const pushHeap = (item: [number, number, number, number]) => {
    heap.push(item);
    heap.sort((a, b) => a[0] - b[0]); // 簡化版,大規模應用請改用完整 MinHeap
  };
  pushHeap([manhattan(start, end), 0, start.row, start.col]);

  while (heap.length > 0) {
    const [, gCost, r, c] = heap.shift()!;
    if (r === end.row && c === end.col) return gCost;
    if (gCost > g[r][c]) continue; // Lazy Deletion

    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
      if (grid[nr][nc] === 1) continue; // 障礙物

      const newG = g[r][c] + 1;
      if (newG < g[nr][nc]) {
        g[nr][nc] = newG;
        const h = manhattan({ row: nr, col: nc }, end);
        pushHeap([newG + h, newG, nr, nc]);
      }
    }
  }

  return -1; // 不可達
}

C++ 對照

Dijkstra(priority_queue + greater<>

#include <vector>
#include <queue>
#include <climits>
using namespace std;

using Edge = pair<int, int>; // {鄰居節點, 邊權重}
using PII  = pair<int, int>; // {距離, 節點}

vector<int> dijkstra(int n, vector<vector<Edge>>& graph, int src) {
    vector<int> dist(n + 1, INT_MAX);
    dist[src] = 0;

    // Min-Heap:以 greater<PII> 使小的元素優先
    priority_queue<PII, vector<PII>, greater<PII>> pq;
    pq.push({0, src});

    while (!pq.empty()) {
        auto [d, u] = pq.top();
        pq.pop();
        if (d > dist[u]) continue; // Lazy Deletion

        for (auto [v, w] : graph[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}

Bellman-Ford(C++)

#include <vector>
#include <climits>
using namespace std;

struct WeightedEdge { int from, to, weight; };

pair<vector<int>, bool> bellmanFord(
    int n,
    const vector<WeightedEdge>& edges,
    int src
) {
    vector<int> dist(n + 1, INT_MAX);
    dist[src] = 0;

    for (int i = 0; i < n - 1; i++) {
        bool updated = false;
        for (const auto& [from, to, w] : edges) {
            if (dist[from] != INT_MAX && dist[from] + w < dist[to]) {
                dist[to] = dist[from] + w;
                updated = true;
            }
        }
        if (!updated) break; // 提前終止
    }

    bool negCycle = false;
    for (const auto& [from, to, w] : edges) {
        if (dist[from] != INT_MAX && dist[from] + w < dist[to]) {
            negCycle = true;
            break;
        }
    }
    return {dist, negCycle};
}

Floyd-Warshall(C++)

#include <vector>
using namespace std;

vector<vector<long long>> floydWarshall(int n, vector<tuple<int,int,int>>& edges) {
    const long long INF = 1e18;
    vector<vector<long long>> dist(n, vector<long long>(n, INF));
    for (int i = 0; i < n; i++) dist[i][i] = 0;
    for (auto [u, v, w] : edges) dist[u][v] = min(dist[u][v], (long long)w);

    // k 在最外層是關鍵!
    for (int k = 0; k < n; k++)
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                if (dist[i][k] < INF && dist[k][j] < INF)
                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);

    return dist;
}

複雜度分析

演算法時間複雜度空間複雜度負權邊負環偵測適用場景
Dijkstra(Min-Heap)O((V+E) log V)O(V+E)不支援不支援稠密/稀疏圖,非負權
Bellman-FordO(VE)O(V)支援支援含負權邊,小圖
SPFA(Bellman-Ford 優化)O(kE) 均攤,O(VE) 最壞O(V+E)支援支援競程中常見,不穩定
Floyd-WarshallO(V³)O(V²)支援支援(對角線)全源,V ≤ 500
A*O(E log V) 最優O(V)不支援不支援有啟發函數的圖搜尋

實務選擇原則

  • 大多數面試題用 Dijkstra(無負權邊)
  • 有負權邊且圖不大用 Bellman-Ford
  • 需要所有點對距離用 Floyd-Warshall(V ≤ 500)
  • 地圖/網格有終點座標可估算距離時用 A*

變體與延伸

0-1 BFS

當圖的邊權重只有 0 和 1 時,可用雙端佇列(Deque)替代 Min-Heap 達到 O(V+E) 的效率:

// 0-1 BFS:邊權為 0 的節點從前端插入,邊權為 1 的從後端插入
function zeroOneBFS(
  graph: { to: number; weight: 0 | 1 }[][],
  n: number,
  src: number
): number[] {
  const dist = new Array(n).fill(Infinity);
  dist[src] = 0;
  const deque: number[] = [src];

  while (deque.length > 0) {
    const u = deque.shift()!;

    for (const { to: v, weight: w } of graph[u]) {
      const newDist = dist[u] + w;
      if (newDist < dist[v]) {
        dist[v] = newDist;
        if (w === 0) deque.unshift(v); // 零權邊 → 從前端插入
        else deque.push(v);             // 一權邊 → 從後端插入
      }
    }
  }

  return dist;
}

應用場景:LeetCode 1368(最少花費使所有格子聯通),邊的花費只有 0(順方向)或 1(逆方向)。

多源最短路徑(虛擬超級起點)

當有多個起點時,建立一個「虛擬超級起點」,從該點到每個真實起點連一條權重為 0 的邊,再跑一次 Dijkstra:

// 多源最短路:新增虛擬節點 superSrc(索引 n),連接所有真實起點
// graph 節點為 0..n-1,超級起點為 n
function multiSourceDijkstra(
  graph: Map<number, [number, number][]>,
  n: number,
  sources: number[]
): number[] {
  const superSrc = n;
  graph.set(superSrc, sources.map(src => [src, 0])); // 零權邊
  const dist = dijkstra(graph, n + 1, superSrc);
  return dist.slice(0, n); // 去掉超級起點本身的距離
}

面試考點:演算法選擇決策樹

有負權邊?
├── 是 → 需要偵測負環?
│         ├── 是 → Bellman-Ford(第 V 次鬆弛仍更新 = 負環)
│         └── 否 → SPFA 或 Bellman-Ford
└── 否 → 需要全源最短路徑?
          ├── 是 → V ≤ 500?
          │         ├── 是 → Floyd-Warshall(O(V³))
          │         └── 否 → 對每個節點各跑一次 Dijkstra
          └── 否 → 有啟發函數(如地圖座標)?
                    ├── 是 → A*(更快找到單一目標)
                    └── 否 → Dijkstra(Min-Heap,O((V+E) log V))

常見面試陷阱

  1. Dijkstra 遇到負權邊:貪心假設失效,dist 可能錯誤,必須改用 Bellman-Ford
  2. Floyd-Warshall 的 k 迴圈順序:k 不在最外層會導致部分中繼點未被考慮,結果錯誤
  3. Bellman-Ford 的副本問題:LeetCode 787 等「限制邊數」的題目,每輪鬆弛必須用上一輪的 dist 副本,避免同輪連鎖更新
  4. JavaScript 的 shift() 效能:模擬佇列時 shift() 是 O(n),大規模 Dijkstra 請用真正的 MinHeap

LeetCode 精選練習

743. Network Delay Time(M)— Dijkstra 單源最短路

給定 n 個節點的有向加權圖與起點 k,求信號從 k 傳播到所有節點的最長時間(即最遠距離)。不可達則回傳 -1。

// 思路:Dijkstra 從 k 出發,取所有節點最短距離的最大值
// 時間:O((V+E) log V),空間:O(V+E)

function networkDelayTime(times: number[][], n: number, k: number): number {
  // 建圖
  const graph = new Map<number, [number, number][]>();
  for (let i = 1; i <= n; i++) graph.set(i, []);
  for (const [u, v, w] of times) graph.get(u)!.push([v, w]);

  // Dijkstra(簡化版 heap,面試可用)
  const dist = new Array(n + 1).fill(Infinity);
  dist[k] = 0;
  const heap: [number, number][] = [[0, k]];

  while (heap.length > 0) {
    heap.sort((a, b) => a[0] - b[0]);
    const [d, u] = heap.shift()!;
    if (d > dist[u]) continue;
    for (const [v, w] of graph.get(u)!) {
      if (dist[u] + w < dist[v]) {
        dist[v] = dist[u] + w;
        heap.push([dist[v], v]);
      }
    }
  }

  const ans = Math.max(...dist.slice(1));
  return ans === Infinity ? -1 : ans;
}
// 測試:times=[[2,1,1],[2,3,1],[3,4,1]], n=4, k=2 → 2

787. Cheapest Flights Within K Stops(M)— Bellman-Ford 限制版

求從 src 到 dst 最多中轉 k 站(即最多走 k+1 條邊)的最便宜費用。

// 思路:Bellman-Ford 只做 k+1 次鬆弛,每次代表多走一條邊
// 關鍵:每輪必須用上一輪的 dist 副本,避免同輪連鎖更新!
// 時間:O(k * E),空間:O(n)

function findCheapestPrice(
  n: number,
  flights: number[][],
  src: number,
  dst: number,
  k: number
): number {
  let dist = new Array(n).fill(Infinity);
  dist[src] = 0;

  for (let i = 0; i <= k; i++) {
    // 複製本輪 dist,避免在同一輪內連鎖更新
    const temp = [...dist];
    for (const [from, to, price] of flights) {
      if (dist[from] !== Infinity && dist[from] + price < temp[to]) {
        temp[to] = dist[from] + price;
      }
    }
    dist = temp;
  }

  return dist[dst] === Infinity ? -1 : dist[dst];
}
// 測試:n=4, flights=[[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]]
//       src=0, dst=3, k=1 → 700

1334. Find the City With the Smallest Number of Neighbors(M)— Floyd-Warshall

找到在距離閾值內鄰居城市數量最少的城市(若並列,取編號最大的)。

// 思路:Floyd-Warshall 建全源最短路矩陣,再統計每個城市的鄰居數
// 時間:O(V³),空間:O(V²)

function findTheCity(
  n: number,
  edges: number[][],
  distanceThreshold: number
): number {
  const INF = Infinity;
  const dist: number[][] = Array.from({ length: n }, (_, i) =>
    Array.from({ length: n }, (_, j) => (i === j ? 0 : INF))
  );

  for (const [u, v, w] of edges) {
    dist[u][v] = w;
    dist[v][u] = w; // 無向圖
  }

  // Floyd-Warshall(k 在最外層)
  for (let k = 0; k < n; k++)
    for (let i = 0; i < n; i++)
      for (let j = 0; j < n; j++)
        if (dist[i][k] !== INF && dist[k][j] !== INF)
          dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);

  // 統計每個城市閾值內的鄰居數,取鄰居最少且編號最大的
  let result = -1;
  let minNeighbors = Infinity;

  for (let i = 0; i < n; i++) {
    let count = 0;
    for (let j = 0; j < n; j++) {
      if (i !== j && dist[i][j] <= distanceThreshold) count++;
    }
    // 並列時取編號最大(>=),即 i 較大時也更新
    if (count <= minNeighbors) {
      minNeighbors = count;
      result = i;
    }
  }

  return result;
}
// 測試:n=4, edges=[[0,1,3],[1,2,1],[1,3,4],[2,3,1]], distanceThreshold=4 → 3

778. Swim in Rising Water(H)— 格上 Dijkstra

雨水高度上漲,你在 (0,0) 到 (n-1,n-1),求最小的時間 t(水位 t),使你能完成旅程。

// 思路:Dijkstra 的邊權 = 路徑上的最大格子值(瓶頸最小化)
// dist[r][c] = 到達 (r,c) 的最小最大高度
// 時間:O(n² log n),空間:O(n²)

function swimInWater(grid: number[][]): number {
  const n = grid.length;
  const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
  const dist: number[][] = Array.from({ length: n }, () =>
    new Array(n).fill(Infinity)
  );
  dist[0][0] = grid[0][0];

  // [最大高度, row, col]
  const heap: [number, number, number][] = [[grid[0][0], 0, 0]];

  while (heap.length > 0) {
    heap.sort((a, b) => a[0] - b[0]);
    const [cost, r, c] = heap.shift()!;
    if (r === n - 1 && c === n - 1) return cost;
    if (cost > dist[r][c]) continue;

    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;
      if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;

      // 瓶頸:整段路徑的最大高度 = max(已知最大高度, 下一格高度)
      const newCost = Math.max(dist[r][c], grid[nr][nc]);
      if (newCost < dist[nr][nc]) {
        dist[nr][nc] = newCost;
        heap.push([newCost, nr, nc]);
      }
    }
  }

  return dist[n-1][n-1];
}
// 測試:grid=[[0,2],[1,3]] → 3

1631. Path With Minimum Effort(M)— 最小化最大邊權

路徑的「努力值」為路徑上相鄰格子高度差的最大值,求從 (0,0) 到 (rows-1,cols-1) 的最小努力值。

// 思路:Dijkstra 變體,dist[r][c] = 到達 (r,c) 的最小努力值(最大高度差)
// 時間:O(V log V),V = rows * cols,空間:O(V)

function minimumEffortPath(heights: number[][]): number {
  const rows = heights.length;
  const cols = heights[0].length;
  const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
  const dist: number[][] = Array.from({ length: rows }, () =>
    new Array(cols).fill(Infinity)
  );
  dist[0][0] = 0;

  const heap: [number, number, number][] = [[0, 0, 0]]; // [effort, row, col]

  while (heap.length > 0) {
    heap.sort((a, b) => a[0] - b[0]);
    const [effort, r, c] = heap.shift()!;
    if (r === rows - 1 && c === cols - 1) return effort;
    if (effort > dist[r][c]) continue;

    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;

      // 本段努力值 = 高度差;整段路徑努力值 = 歷史最大值 vs 本段
      const segEffort = Math.abs(heights[nr][nc] - heights[r][c]);
      const newEffort = Math.max(dist[r][c], segEffort);

      if (newEffort < dist[nr][nc]) {
        dist[nr][nc] = newEffort;
        heap.push([newEffort, nr, nc]);
      }
    }
  }

  return 0;
}
// 測試:heights=[[1,2,2],[3,8,2],[5,3,5]] → 2

總結

最短路徑演算法的三大核心是:

  • Dijkstra:貪心 + Min-Heap,O((V+E) log V),適合無負權邊的單源最短路。面試出現頻率最高。
  • Bellman-Ford:動態規劃,O(VE),支援負權邊且能偵測負環。LeetCode 787「限制跳數」是其典型應用。
  • Floyd-Warshall:三層 DP,O(V³),一次求出所有節點對距離。適合 V ≤ 500 的小圖。

記住鬆弛操作是所有演算法的共同語言,三者的差異只是鬆弛的順序與次數。掌握「何時用哪個演算法」的決策邏輯,是拿下面試最短路徑題的關鍵。

希望這篇文章能幫助你系統掌握最短路徑演算法。如有任何問題或疑惑,歡迎到聯絡頁留言!

下一篇將進入拓撲排序(Topological Sort),從有向無環圖(DAG)出發,解析課程表、任務排程等依賴問題——敬請期待!

上一篇:BFS 與 DFS — 廣度優先與深度優先搜尋完整教學
BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。