最短路徑演算法 — 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-Ford | O(VE) | O(V) | 支援 | 支援 | 含負權邊,小圖 |
| SPFA(Bellman-Ford 優化) | O(kE) 均攤,O(VE) 最壞 | O(V+E) | 支援 | 支援 | 競程中常見,不穩定 |
| Floyd-Warshall | O(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))
常見面試陷阱:
- Dijkstra 遇到負權邊:貪心假設失效,dist 可能錯誤,必須改用 Bellman-Ford
- Floyd-Warshall 的 k 迴圈順序:k 不在最外層會導致部分中繼點未被考慮,結果錯誤
- Bellman-Ford 的副本問題:LeetCode 787 等「限制邊數」的題目,每輪鬆弛必須用上一輪的 dist 副本,避免同輪連鎖更新
- 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 — 廣度優先與深度優先搜尋完整教學