Dijkstra’s Algorithm

圖論優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 用優先佇列解非負權重的單源最短路徑。 另見:Dijkstra_examples.md — 撐起這些模板的十一題詳解;shortest_path_comparison.md — 該挑哪個演算法;Bellman-Ford.md — 權重可能為負時用它;Floyd-Warshall.md — 需要全點對時用它;heap.md — 底下那個優先佇列。

LeetCode 題目清單

總覽

Dijkstra(戴克斯特拉)演算法是一個貪婪演算法,專門解邊權 NON-NEGATIVE(非負)圖上的單源最短路徑問題。它會算出起點(source)到圖中所有其他節點的最短路徑。

關鍵性質

  • 時間複雜度:用二元堆積是 O((V + E) log V),用陣列是 O(V²)
  • 空間複雜度:O(V),存距離陣列跟走訪集合
  • 核心想法:每次貪婪地挑出「還沒定案、距離最小」的節點
  • 什麼時候用:非負權重的單源最短路徑
  • 限制:不能處理負邊權、最大化路徑上的收益總和(最長路徑),也不能處理「已用過的格子決定接下來能走哪裡」的路徑(LC 1219) —— 見 Dijkstra 不適用的情況

核心特徵

  • 貪婪演算法:永遠先挑距離最小的節點
  • 優先佇列:用最小堆積(min-heap)快速取出最小值
  • 鬆弛(Relaxation):找到更短的路徑就更新距離
  • 定案(Finalization):一個節點一旦被走訪過,它的距離就是最佳解

參考資料

題型分類

分類 1:經典最短路徑

  • 說明:標準的單源最短路徑題
  • 例子:LC 743(Network Delay)、LC 1514(Path with Max Probability)
  • 模式:直接套 Dijkstra

分類 2:帶限制的最短路徑 ⚠️ Dijkstra 變形

  • 說明:最短路徑多了一個限制維度(轉機次數、障礙物、鑰匙、時間)
  • 例子:LC 787(Cheapest Flights K Stops)、LC 1293(Shortest Path K Obstacle Removal)、LC 864(Get All Keys)、LC 1928(Minimum Cost K Waypoints)
  • 模式:二維狀態 Dijkstra — 狀態是 (cost, node, constraint),不是 (cost, node)
  • 為什麼算變形:同一個節點在不同限制值下是不同的狀態。標準的 visited[node] 或 dist[node] 剪枝在這裡是錯的 — 它會丟掉那些「用不同剩餘額度抵達同一節點」的合法路徑。
  • 剪枝規則:best[(node, constraint)] <= cost(二維 best map,不是一維 dist 陣列)

分類 3:格子上的最短路徑

  • 說明:在二維格子裡找最佳路徑
  • 例子:LC 64(Minimum Path Sum)、LC 1631(Path Min Effort)、LC 778(Swim in Rising Water)
  • 模式:在隱式圖上跑 Dijkstra(把格子當節點)
  • ⚠️ 特別注意:LC 64 可以純 DP 解,不必動用 Dijkstra(見下)

分類 4:多源最短路徑

  • 說明:從多個起點出發找最短路徑
  • 例子:LC 2812(Find Safest Path)、LC 1162(As Far from Land)
  • 模式:一開始就把多個源頭全丟進去,或建一個超級源點

分類 5:與時間相關的最短路徑

  • 說明:路徑成本會隨時間或順序改變
  • 例子:LC 2045(Second Minimum Time)、LC 882(Reachable Nodes)
  • 模式:在優先佇列裡一起追蹤時間/狀態

該不該用 Dijkstra:兩個問題

這份表以前有九個小節在用各種說法吵 LC 1631。其實都可以收斂成兩個問題 —— 任何最短路徑題都能拿來問一遍 —— 再加一個值得知道的替代視角。

1) 成本是可加的嗎?格子能不能排出順序?— LC 64 vs LC 1631 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

問題:跑 Dijkstra 真的需要 dist[r][c](記錄到每格的最小成本)嗎?還是純 DP 就夠了?

答案:看移動方向:

LC 64: Minimum Path Sum ✅ 純 DP 就夠

text
Movement: RIGHT only ↓ or DOWN only →
  • DP 為什麼行得通:(i,j) 這格只可能從 (i-1,j) 或 (i,j-1) 過來
  • 不需要 dist[][]:每格照拓撲順序剛好算一次
  • 不會回頭:一格算完之後,不可能再冒出「更好的路徑」
  • 解法:單純的二維 DP,或壓成 O(min(m,n)) 空間的一維 DP
java
// Pure DP - NO dist[][] needed
public int minPathSum(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    
    // First column: only from above
    for (int i = 1; i < m; i++)
        dp[i][0] = dp[i-1][0] + grid[i][0];
    
    // First row: only from left
    for (int j = 1; j < n; j++)
        dp[0][j] = dp[0][j-1] + grid[0][j];
    
    // Fill rest
    for (int i = 1; i < m; i++)
        for (int j = 1; j < n; j++)
            dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
    
    return dp[m-1][n-1];
}
// Time: O(m*n), Space: O(min(m,n))

LC 1631: Path With Minimum Effort ⚠️ 需要 Dijkstra + dist[][]

text
Movement: UP, DOWN, LEFT, RIGHT (all 4 directions)
  • 為什麼需要 Dijkstra:同一格可能從很多條路徑抵達,而且更好的路徑可能晚一點才出現
  • dist[][] 不能省:它記錄每格「目前找到的最佳成本」
  • 會回頭重訪:四個方向都能走的時候,你可能用更好的成本再次踩到同一格
  • 解法:Dijkstra 搭配 dist[][] + PriorityQueue
java
// Dijkstra + dist[][] - NECESSARY for 4-directional movement
public int minimumEffortPath(int[][] heights) {
    int m = heights.length, n = heights[0].length;
    
    // dist[r][c] = minimum effort found so far to reach (r,c)
    int[][] dist = new int[m][n];
    for (int[] row : dist)
        Arrays.fill(row, Integer.MAX_VALUE);
    
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[2], b[2]));
    pq.offer(new int[]{0, 0, 0});
    dist[0][0] = 0;
    
    int[][] dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int r = cur[0], c = cur[1], cost = cur[2];
        
        // Already processed with better cost
        if (cost > dist[r][c]) continue;
        
        if (r == m-1 && c == n-1) return cost;
        
        for (int[] d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            if (nr >= 0 && nr < m && nc >= 0 && nc < n) {
                int newCost = Math.max(cost, Math.abs(heights[nr][nc] - heights[r][c]));
                if (newCost < dist[nr][nc]) {
                    dist[nr][nc] = newCost;
                    pq.offer(new int[]{nr, nc, newCost});
                }
            }
        }
    }
    return -1;
}
// Time: O(m*n*log(m*n)), Space: O(m*n)

對照表 — LC 64 vs LC 1631

題目 移動方向 成本模型 最佳解法 需要 dist[][]? 需要 visited?
LC 64 右 + 下 加總 二維 DP ❌ 不用 ❌ 不用
LC 1631 四方向 差值取最大 Dijkstra ✅ 要 ✅ 要(靠 dist 檢查)
LC 1263 四方向 加總成本 Dijkstra ✅ 要 ✅ 要(靠 dist 檢查)

有兩個推論值得白紙黑字寫下來,因為這正是兩種最常見的翻車方式:

為什麼 DP 救不了 LC 1631。 答:問題出在移動方向:

  • LC 64:只能往右/往下 → 存在拓撲順序 → DP 可行 ✅
  • LC 1631:上下左右都能走 → 有環 → DP 失效 ❌

四方向移動會產生循環依賴:

text
(1,1) → (1,2) → (2,2) → (2,1) → (1,1)

DP 要求依賴關係構成 DAG(無環),所以這題只能用 Dijkstra 或二分搜尋。

為什麼「cost」和「effort」不是同一種量。 答:不同題量的是不同東西:

  • Cost(LC 64、1263):路徑上所有值的總和 = cost += value
  • Effort(LC 1631):相鄰格子差值的最大值 = effort = max(effort, |diff|)

Cost 可加,effort 不可加。就是這個不可加性讓 DP 掛掉。

2) 你需要 dist[],還是 visited[] 就夠? 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

text
dist[r][c] = "What's the MINIMUM cost I've found SO FAR to reach (r,c)?"
  • 初始化:dist[r][c] = Integer.MAX_VALUE(未知)
  • 更新:從 PQ 彈出某格、成本為 C 時,檢查 if (C > dist[r][c]) continue;
    • 條件成立表示我們早就找到更好的路徑 → 跳過不處理
    • 這等於自動避免重複處理,不需要另外開 visited 陣列
  • 什麼時候非要不可:同一格有多條路徑可達 → 需要 Dijkstra 的鬆弛機制

答:不用,兩者擇一:

  • 選項 A:dist[][] → 處理前檢查 if (newCost < dist[r][c])
  • 選項 B:visited[] → 從 PQ 第一次彈出後就標記已走訪

兩種都能避免重複處理同一格。哪個你看得順眼就用哪個 —— 但它們不是永遠可以互換。下面會說明什麼時候 best[]/dist[] 是真的必要,什麼時候可以簡化成「直接拿下一條候選路徑跟目前(剛彈出)這條比」。

Dijkstra 在看候選邊 cur_node -> nxt_node 時,可以做兩種完全不同的比較:

類型 1:跟 best[] 比 類型 2:下一條路徑跟剛彈出的 cur 路徑比
比的是什麼 candidate_value 對上 best[nxt_node](該節點/狀態歷來記錄過的最佳值) 什麼都沒存 —— candidate_value 直接由 cur_node 已定案的值推出來,沒有查表這回事
檢查發生在什麼時候 推進堆積之前(鬆弛步驟) 彈出之後 —— 只有一個 visited[node] 布林關卡,完全不比值
為什麼需要/為什麼行得通 整趟跑下來,同一個節點(或節點+限制狀態)可能被用不同的值抵達很多次;你得記住目前最好的那個,才知道新路徑是不是真的更好 Dijkstra 的最小堆積不變量保證一個節點第一次被彈出時就已經是全域最佳,所以彈出後根本沒東西好比 —— 之後那些比較差的重複項,visited 檢查一律跳過
會跨越多次更新持續存在嗎? ✅ 會 —— 一個節點定案前,best[node] 可能被覆寫好幾次 ❌ 不會 —— 一個節點只寫一次(visited[node] = true),之後再也不動
什麼時候非用不可… 狀態多了一個維度((node, constraint) —— 同一個節點在好幾個不同限制值下都算「合法抵達」),或者你需要偵測平手(例如數路徑條數),或者你想在推進堆積前就把不會變好的候選擋掉 狀態就是每個節點一個純量、邊權非負、沒有額外限制維度 —— 也就是最單純的單源情況
用錯了會不會默默出錯 不會(永遠正確,只是多吃點記憶體) ❌ 在帶限制的題目(LC 787 那類)只用 visited[] 是錯的 —— 它會抹掉那個額外維度,丟掉合法路徑(具體推演見 LC 787)

一句話講完核心概念:

best[] 回答的是:「這個候選,比我對這個狀態看過的任何東西都好嗎?」 —— 只要一個狀態可以合法地用不同的值重訪,就需要它。 只用 visited[] 回答的是:「這個狀態已經定案了嗎?」 —— 只有在堆積的彈出順序保證(第一次彈出=最佳)完全涵蓋整個狀態時才夠,也就是沒有額外限制維度的情況。

經典 Dijkstra 題目分類:

類型 LC # 題目 原因
類型 1 — 需要 best[]/dist[] 743 Network Delay Time 標準單源,推進堆積前先鬆弛
1514 Path with Maximum Probability best[]/max_prob[] 記錄每個節點目前的最大乘積
1976 Number of Ways to Arrive at Destination 同時需要 dist[] 跟 ways[] —— 必須抓到精確的平手(==),沒有存下來的值就辦不到
787 Cheapest Flights Within K Stops 必須用二維 best[(node, stops)] —— 只用 visited[node] 可證明是錯的(見 LC 787 的推演)
1293 / 864 / 2093 帶限制狀態的 Dijkstra 變形 跟 787 同理 —— 多一個限制維度就代表一個節點會有多個合法的定案狀態
類型 2 — 只用 visited[] 就夠 1631 Path With Minimum Effort 變形 2(visited[][])—— 每格狀態是純量,第一次彈出就是最佳 effort
778 Swim in Rising Water visited[][] 標記格子已定案;下一條路徑的值 = max(目前路徑值, 下一格高度)
743 Network Delay Time(另一種寫法) LC 743 展示的「visited 集合版本」—— 跟 dist[] 等價,只是改成彈出後檢查而非推進前檢查
2290 Minimum Obstacle Removal 權重只有 0/1 → 用雙端佇列跑 0-1 BFS + visited[] 也行,不需要值表

經驗法則:如果「光靠 node 就能完整描述我在搜尋中的位置嗎?」這個問題答案是是,那只用 visited[] 就安全。一旦答案變成「不行,我還得知道自己用掉了幾次轉機/幾把鑰匙/清了幾個障礙」,就必須升級成以 (node, constraint) 為鍵的 best[]/dist[]。

答:對!if (cost > dist[r][c]) continue; 這個檢查就是你的 visited 機制:

  • 第一次彈出 (r,c):cost == dist[r][c] → 處理
  • 之後再彈出 (r,c):cost > dist[r][c] → 跳過(等同「已走訪」)

所以你不用多開一個陣列,就享有 visited[] 的語意。


3) 什麼時候併查集是更好的視角

答:以下情況用併查集:

  • 你習慣自己把邊列表建出來
  • 你想把問題看成圖的連通性問題
  • 你在練 Kruskal 演算法

兩者時間複雜度一樣是 O(m×n×log(m×n)),但格子題通常 Dijkstra 比較直覺。

模板與演算法

模板對照表

模板類型 適用情境 追蹤的狀態 什麼時候用
基本 Dijkstra 標準最短路徑 (distance, node) 沒有額外限制
帶限制路徑 有上限的路徑 (cost, node, constraint) K 次轉機、預算
格子 Dijkstra 二維格子移動 (cost, x, y) 矩陣題
多源 多個起點 (dist, node, source) 多個源頭
時間變動 成本隨時間變 (time, node, state) 動態成本

通用 Dijkstra 模板 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

python
import heapq
import collections

def dijkstra(n, edges, src, dst):
    # Build adjacency list
    graph = collections.defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
    
    # Min heap: (distance, node)
    pq = [(0, src)]
    # Distance array
    dist = [float('inf')] * n
    dist[src] = 0
    # Visited set (optional but recommended)
    visited = set()
    
    while pq:
        d, u = heapq.heappop(pq)
        
        # Skip if already processed with better distance
        if u in visited:
            continue
        visited.add(u)
        
        # Found destination
        if u == dst:
            return d
        
        # Relax edges
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))
    
    return dist[dst] if dist[dst] != float('inf') else -1

核心步驟 — 七步走完整個迴圈(LC 743)

同一個迴圈,以 LC 743 Network Delay Time 來讀,起點是題目給的節點 k。執行流程:

  1. 從起點 k 出發,初始距離為 0。
  2. 把起點 (0, k) 放進 Min Heap。
  3. 從 Min Heap 取出目前累積距離最小的節點。
  4. 探索這個節點的所有 neighbors。
  5. 計算到每個 neighbor 的新距離:cur_dist + edge_cost。
  6. 將尚未確定最短距離的 neighbor 放入 Min Heap。
  7. 持續執行,直到 Min Heap 為空。

為什麼第一次 pop 就是最終答案:每個 heap entry 都帶著從 k 出發的完整路徑成本,而 heap 永遠先吐出最小的那個 — 沒有負權邊時,之後 pop 出來的不可能更小。所以一個節點第一次被 pop 時就確定了最短距離,之後再 pop 到同一個節點都是過期的 entry,直接跳過。

陷阱:這不是逐層 BFS。每一輪只 pop 一個節點;用 for _ in range(len(q)) 的逐層迴圈會依跳數而非成本處理節點,破壞上面的保證。

python
# LC 743 - Network Delay Time
# IDEA: Dijkstra (min-heap PQ + BFS); stop as soon as all n nodes are finalized
# time = O((V + E) log V), space = O(V + E)
import heapq
from collections import defaultdict

def network_delay_time(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))          # u -> (neighbor, travel time)

    min_heap = [(0, k)]                  # steps 1-2: (accumulated time, node)
    visited = set()                      # nodes whose shortest time is finalized

    while min_heap:                      # step 7
        time, node = heapq.heappop(min_heap)   # step 3
        if node in visited:              # stale entry: already finalized cheaper
            continue
        visited.add(node)
        if len(visited) == n:            # last node reached = network delay
            return time
        for neighbor, weight in graph[node]:   # step 4
            if neighbor not in visited:  # step 6
                heapq.heappush(min_heap, (time + weight, neighbor))  # step 5
    return -1                            # heap emptied, some node unreachable

模板 1:基本 Dijkstra

python
def dijkstra_basic(n, edges, src):
    """Find shortest paths from src to all nodes"""
    graph = collections.defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
    
    dist = [float('inf')] * n
    dist[src] = 0
    pq = [(0, src)]  # (distance, node)
    
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:  # Already processed
            continue
        
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))
    
    return dist

模板 2:帶限制的 Dijkstra(二維狀態變形) 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

核心想法 —— 為什麼這不是標準 Dijkstra:

標準 Dijkstra 帶限制的 Dijkstra
狀態 (cost, node) (cost, node, constraint)
狀態空間 一維 —— 每個節點一筆 二維 —— 每組 (node, constraint) 一筆
剪枝 dist[node] <= cost best[(node, stops)] <= cost
第一次彈出的不變量 node 第一次彈出=全域最佳 (node, stops) 第一次彈出=該 stops 值下的最佳
visited[node] 有用嗎? ✅ 有 ❌ 沒有 —— 同一節點在不同轉機次數下都合法

為什麼 visited[node]/dist[node] 會壞掉:

text
Example: src=0, dst=3, K=2
  Path A: 0→1→3  cost=900  stops=1  ← fewer stops but more expensive
  Path B: 0→1→2→3  cost=210  stops=2  ← more stops but cheaper

Standard dist[1] would be finalized at first pop (cost=100).
Path B would try to re-expand node 1 at stops=1, but dist[1] check blocks it.
→ Wrong: path B never explored, answer is incorrect.

With best[(node, stops)]: (node=1, stops=0) and (node=1, stops=1) are DIFFERENT states.
Both get explored independently. Correct answer found.
python
def dijkstra_constrained(n, edges, src, dst, k):
    graph = collections.defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))

    # (cost, node, stops_used)
    pq = [(0, src, 0)]

    # KEY: 2D best-map — best[(node, stops)] = min cost to reach node using exactly 'stops' edges
    # This replaces the 1D dist[] array used in standard Dijkstra
    best = {}

    while pq:
        cost, u, stops = heapq.heappop(pq)

        # First pop of (u, dst) is optimal (min-heap guarantee for this state)
        if u == dst:
            return cost

        # Constraint exceeded — prune this branch
        if stops > k:
            continue

        # 2D pruning: skip if we've already reached (node, stops) cheaper
        if (u, stops) in best and best[(u, stops)] <= cost:
            continue
        best[(u, stops)] = cost

        for v, w in graph[u]:
            heapq.heappush(pq, (cost + w, v, stops + 1))

    return -1

通用的帶限制 Dijkstra 骨架:

python
# Replace 'stops' with whatever constraint your problem has:
# - stops remaining (LC 787)
# - obstacles budget (LC 1293)
# - keys bitmask (LC 864)
# - discount count (LC 2093)

pq = [(0, src, initial_constraint)]
best = {}   # best[(node, constraint)] = min cost

while pq:
    cost, node, constraint = heapq.heappop(pq)
    if node == dst: return cost
    if constraint is exhausted: continue
    if (node, constraint) in best and best[(node, constraint)] <= cost: continue
    best[(node, constraint)] = cost
    for nei, w in graph[node]:
        heapq.heappush(pq, (cost + w, nei, updated_constraint))

相似題目(同樣的二維狀態模式):

LC # 題目 限制維度 狀態
787 Cheapest Flights K Stops 已用轉機次數 (0…K) (node, stops)
1293 Shortest Path K Obstacle Removal 已清障礙數 (0…K) (node, obstacles)
864 Shortest Path to Get All Keys 已收集的鑰匙(bitmask) (node, keys)
2093 Minimum Cost to Reach City With Discounts 已用折扣次數 (0…K) (node, discounts)
1928 Min Cost to Reach Destination in Time 剩餘時間 (node, time)

模板 3:格子上的 Dijkstra 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

python
def dijkstra_grid(grid):
    """Find minimum cost path in 2D grid"""
    rows, cols = len(grid), len(grid[0])
    
    # Min heap: (cost, row, col)
    pq = [(0, 0, 0)]
    # Distance matrix
    dist = [[float('inf')] * cols for _ in range(rows)]
    dist[0][0] = 0
    
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
    
    while pq:
        cost, r, c = heapq.heappop(pq)
        
        if r == rows - 1 and c == cols - 1:
            return cost
        
        if cost > dist[r][c]:
            continue
        
        for dr, dc in directions:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols:
                # Calculate new cost (problem-specific)
                new_cost = max(cost, abs(grid[nr][nc] - grid[r][c]))
                
                if new_cost < dist[nr][nc]:
                    dist[nr][nc] = new_cost
                    heapq.heappush(pq, (new_cost, nr, nc))
    
    return -1

模板 4:多源 Dijkstra 優先度 3/5 — 值得會 — 多半是必備模式的變形

python
def dijkstra_multi_source(n, edges, sources):
    """Shortest paths from multiple sources"""
    graph = collections.defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
    
    dist = [float('inf')] * n
    pq = []
    
    # Initialize all sources
    for src in sources:
        dist[src] = 0
        heapq.heappush(pq, (0, src))
    
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue
        
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(pq, (dist[v], v))
    
    return dist

模板 5:雙向 Dijkstra

python
def dijkstra_bidirectional(n, edges, src, dst):
    """Optimize by searching from both ends"""
    graph = collections.defaultdict(list)
    reverse = collections.defaultdict(list)
    for u, v, w in edges:
        graph[u].append((v, w))
        reverse[v].append((u, w))
    
    def dijkstra_helper(start, adj, other_dist):
        dist = [float('inf')] * n
        dist[start] = 0
        pq = [(0, start)]
        visited = set()
        min_path = float('inf')
        
        while pq:
            d, u = heapq.heappop(pq)
            if u in visited:
                continue
            visited.add(u)
            
            # Check if we can form a complete path
            if other_dist[u] != float('inf'):
                min_path = min(min_path, d + other_dist[u])
            
            for v, w in adj[u]:
                if dist[u] + w < dist[v]:
                    dist[v] = dist[u] + w
                    heapq.heappush(pq, (dist[v], v))
        
        return dist, min_path
    
    # Run both directions
    dist_fwd, path1 = dijkstra_helper(src, graph, [float('inf')] * n)
    dist_bwd, path2 = dijkstra_helper(dst, reverse, dist_fwd)
    
    return min(path1, path2, dist_fwd[dst])

依模式分類的題目

經典最短路徑題

題目 LC # 關鍵技巧 難度
Network Delay Time 743 基本 Dijkstra Medium
Path with Maximum Probability 1514 最大堆積變形 Medium
Find the City With Smallest Number 1334 全點對最短路徑 Medium
Minimum Weighted Subgraph 2203 三個源頭跑 Dijkstra Hard
Number of Ways to Arrive 1976 數最短路徑條數 Medium
Shortest Path in Binary Matrix 1091 格子 Dijkstra Medium

帶限制的路徑題

題目 LC # 關鍵技巧 難度
Cheapest Flights Within K Stops 787 狀態追蹤 Medium
Minimum Cost to Reach City 1928 K 個中繼點 Hard
Shortest Path to Get All Keys 864 狀態 bitmask Hard
Escape a Large Maze 1036 有上限的 BFS/Dijkstra Hard
Minimum Obstacle Removal 2290 0-1 BFS 變形 Hard

格子題

題目 LC # 關鍵技巧 難度
Minimum Path Sum 64 DAG 格子(DP 較佳,Dijkstra 也能解) Medium
Path With Minimum Effort 1631 格子 Dijkstra Medium
Swim in Rising Water 778 最小時間路徑 Hard
Minimum Cost to Make Valid Path 1368 改造過的成本 Hard
Shortest Path in a Grid 1293 K 個障礙 Hard
Trap Rain Water II 407 優先佇列 Hard

多源題

題目 LC # 關鍵技巧 難度
Find Safest Path in Grid 2812 多源初始化 Medium
As Far from Land as Possible 1162 多源 BFS Medium
Shortest Distance from All Buildings 317 跑多次 Dijkstra Hard
Minimum Height Trees 310 找中心點 Medium

與時間/狀態相關的題

題目 LC # 關鍵技巧 難度
Second Minimum Time to Destination 2045 追蹤兩個值 Hard
Reachable Nodes In Subdivided Graph 882 邊的細分 Hard
Minimum Time to Visit All Points 2065 狀態追蹤 Hard
The Maze III 499 字典序路徑 Hard

詳解範例

十一道題放在 Dijkstra_examples.md,依「狀態長什麼樣」分組 —— 因為決定實作方式的就是這件事:

分組 題目 狀態由什麼構成
經典單源 LC 743, 1514, 1976 每個節點一個純量
帶限制的狀態 LC 787 (node, budget) —— 多一個維度
格子 LC 1631, 778, 64, 2290, 1368 一個格子,再加上成本是可加還是取 running max
多源與隱式圖 LC 407, 373 圖從來不是以邊列表給你的

決策框架

模式選擇策略

text
Dijkstra Algorithm Selection Flowchart:

1. What does the path's value do as the path grows?
   ├── MINIMISE a cost that can only grow   → Continue to 2
   ├── MAXIMISE a value that can only shrink AND keeps two arrivals
   │   in the same order (× p with 0 ≤ p ≤ 1, the smallest cell so far)
   │   → max-heap Dijkstra (LC 1514, 1102); with a K-edge / stop limit,
   │     key the state on (node, edges_used) as in step 5 — a likelier
   │     arrival with no edges left must not block a viable one
   └── MAXIMISE a value that keeps growing (a sum of gold / score)
       → not Dijkstra: backtracking (LC 1219), or DP on a DAG —
         see "When Dijkstra does not apply"

2. Are all edge weights non-negative?
   ├── NO → Use Bellman-Ford or SPFA
   └── YES → Continue to 3

3. Single source or multiple sources?
   ├── Multiple → Use Multi-Source Dijkstra (Template 4)
   └── Single → Continue to 4

4. Is it on a graph or grid?
   ├── Grid → Use Grid-based Dijkstra (Template 3)
   └── Graph → Continue to 5

5. Any constraints (K stops, budget, time)?
   ├── YES → Use Constrained Dijkstra (Template 2)
   └── NO → Use Basic Dijkstra (Template 1)

6. Need optimization for large graphs?
   ├── YES → Consider Bidirectional Dijkstra (Template 5)
   └── NO → Use selected template from above

什麼時候用 Dijkstra、什麼時候用 BFS

判準 Dijkstra BFS
邊權 非負且不一樣 全部相同(無權重)或 0/1
資料結構 優先佇列(最小堆積) 佇列(LinkedList)
時間複雜度 O((V + E) log V) O(V + E)
第一次抵達=最短? ❌ 不是(必須靠 PQ 鬆弛) ✅ 是(層數=距離)
「最小成本/權重」 ✅ 用 Dijkstra ❌ 會算錯
「最少步數/移動次數」 ❌ 殺雞用牛刀 ✅ 用 BFS
成本不一的格子 ✅ 在隱式圖上跑 Dijkstra ❌
成本一致的格子 ❌ 多餘的開銷 ✅ BFS

決策規則:每條邊成本都一樣(或都是 1)就用 BFS —— 更簡單而且是 O(V+E)。一旦邊有不同的非負權重,改用 Dijkstra。

常見陷阱:像 LC 279 Perfect Squares 或 LC 752 Open the Lock 這種所有邊成本都是 1 的題目,還硬要用 Dijkstra(PQ)—— 單純 BFS 就夠了,而且更快。

0-1 BFS 特例:如果邊權只有 0 或 1,用雙端佇列 —— 權重 0 的邊往前推、權重 1 的往後推。複雜度跟 BFS 一樣是 O(V+E),又能正確處理兩種權重。例子:LC 2290 Minimum Obstacle Removal。

Dijkstra 不適用的情況 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

Dijkstra 做的每一件事都建立在一個主張上:節點第一次被彈出時,它的值就是最終答案。 這個主張要成立,需要兩件事同時為真,而下面每一條限制,都是其中一件不成立:

  1. 把路徑延長,永遠不會讓它變更好,也不會讓兩個抵達結果的先後對調。 多走一條邊,只會讓你要最小化的成本變大(或讓你要最大化的值變小),而且同一條邊套在同一節點的兩個抵達結果上,原本較好的仍然較好。w ≥ 0 的 + w、0 ≤ p ≤ 1 的 × p、max 和 min 都符合。這樣之後彈出的任何東西,都不可能贏過先彈出的。
  2. 只看節點就知道你在哪裡。 如果接下來能做什麼,取決於你怎麼走到這裡(已經用過哪些格子、還剩幾站、手上有哪些鑰匙),那 dist[node] 就會丟掉仍然需要的路徑。
情況 哪個條件不成立 為什麼第一次彈出不是最終答案 改用 LC
最大化路徑上非負收益的總和(黃金、分數、長度) 1 每多走一格路徑就更好,所以最佳路徑是最後才找到,不是最先 —— 這是最長路徑問題,在一般圖上是 NP-hard 回溯(輸入小時),或圖是 DAG 時依拓撲順序做 DP 1219
每格最多走一次,而且用過的格子決定哪些路徑還走得通 2 真正的狀態是 (格子, 已用過的格子集合);best[cell] 每格只留一條路徑,把其他路徑擋掉 回溯 1219
答案取決於已經看過哪些節點(要走遍所有節點;可以重複造訪) 2 同一個節點、看過的集合不同,就是不同的狀態 節點 ≤ ~20 個時,在 (node, bitmask) 上跑 BFS / Dijkstra 847
有負邊權 1 之後一條負邊可能壓過一個已經定案的節點 Bellman-Ford / SPFA 787
有站數上限(最多 K 個中轉站) 2 同一個節點、剩下的站數較少,就是不同的狀態 在 (node, stops) 上跑 Template 2,或跑 K + 1 輪 Bellman-Ford —— K 個中轉站就是 K + 1 條邊 787
要最小化的成本之外還有時間預算 2 比較便宜的抵達結果,可能已經花掉太多時間而走不完 在 (node, 已用時間) 上跑 Dijkstra,或以時間為索引做 DP 1928
要求第二短 / 第 k 短,不是最短 — 第一次彈出只給出最短的 針對 LC 2045 的「第二個不同的抵達時間」:讓每個節點最多被彈出 k 次,只計算不同的時間(兩條一樣短的路線不算第二短)。這算的是走法(walk),可能重複經過節點 —— 不是第 k 短的簡單路徑(那要用 Yen’s algorithm) 2045
所有邊成本相同,或只有 0 / 1 —(能用,但大材小用) 堆積帶來的好處,佇列一樣能給 BFS / 0-1 BFS(見上) 2290
要求每一對節點 —(能用,跑 V 次) 小圖用 Floyd-Warshall 1334

問題不在「最大化」—— 在於值往哪個方向變。 只要把路徑延長只會讓它變差,而且不會讓兩個抵達結果的先後對調(上面的條件 1),Dijkstra 就能處理最大值:

題目 路徑上的值 延長路徑時 Dijkstra?
LC 1514 Path with Maximum Probability 機率的乘積 ≤ 1 只會變小 ✅ 最大堆積
LC 1102 Path With Maximum Minimum Value 目前為止最小的格子 只會變小 ✅ 最大堆積
LC 1631 Path With Minimum Effort 目前為止最大的一步,要最小化 只會變大 ✅ 最小堆積
LC 1219 Path with Maximum Gold 收集到的黃金總和,要最大化 只會變大 ❌ 回溯

完整的規則,以及條件 2 的 (node, state) 解法,見 shortest_path_comparison.md § D 與 § B。

Path with Maximum Gold — LC 1219

Key point — 答案是以收集黃金計算的最長簡單路徑,而且每格只能用一次,所以上面兩個條件同時不成立:沒有任何 dist[cell] 能代表「走到這格的最佳方式」,因為最先走到它的那條路徑,可能已經用掉了最佳路徑需要的格子。要從每個起點出發、試遍每條簡單路徑,進入格子時標記、離開時還原;黃金格最多 25 個,這樣就夠快。

Reduces to:dfs.md § Template 9: Grid DFS with Restore (Backtracking), 解法放在那裡;標記生命週期的追蹤在 backtrack.md § Template 9。

Dry run —「拿 Dijkstra 求最大值」(以目前黃金數建最大堆積、每格第一次彈出就定案、每格都當一次起點)對上回溯。不管平手時怎麼排序(720 種全試過),貪婪法最多都只到 7,所以失敗不是平手造成的:

text
grid = 1 2 1        cells: (0,0) (0,1) (0,2)
       1 2 1               (1,0) (1,1) (1,2)

start (0,0), max-heap + best[cell]:
  pop (0,0) gold 1   push (0,1)=3, (1,0)=2
  pop (0,1) gold 3   <- 3 beats 2 outright; (0,1) is now FINAL
                        with the path (0,0)->(0,1)
  pop (1,1) gold 5, (1,0) gold 6, (1,2) gold 6, (0,2) gold 7
  best from this start: 7

backtracking            -> 8   (0,0) -> (1,0) -> (1,1) -> (0,1) -> (0,2) -> (1,2)
                               reaches (0,1) FOURTH, through a longer path that
                               best[(0,1)] had already ruled out

Traps

  • 最大堆積加 best[cell] 能通過題目自己的兩個範例(24 和 28),卻在上面的 2×3 網格失敗,不論平手怎麼排序都一樣:範例答對,不代表這個貪婪成立。
  • 複雜度是 O(k · 3^k),k 是黃金格數,不是多項式:第一步之後,每格最多只有 3 個沒走過的鄰居。能過是因為限制條件(k ≤ 25),面試時要講出來。

演算法比較:Dijkstra vs Floyd-Warshall vs Bellman-Ford

這一節原本放的是和其他最短路徑演算法的比較 —— 複雜度、負權重、提前結束、 路徑還原,以及什麼情境該選哪一個 —— 現在統一放在一個地方: shortest_path_comparison.md § Side-by-Side Comparison 與 § Scenario → Algorithm。這份 sheet 的一句話版本:單一起點、權重非負時預設用 Dijkstra;有負權重或跳數限制就改用 Bellman-Ford,小圖上要所有點對就用 Floyd-Warshall,權重全相同就用 BFS(見上面)。

總結與速查

複雜度速查

實作方式 時間複雜度 空間複雜度 備註
陣列版 O(V²) O(V) 稠密圖適用
二元堆積 O((V+E)logV) O(V) 最常見
費氏堆積 O(E + VlogV) O(V) 理論最佳
格子版 O(RC log(RC)) O(RC) R=列數、C=行數

模板速查

模板 最適合 關鍵程式碼模式
基本 標準最短路徑 heapq.heappop(pq) → 鬆弛邊
帶限制 K 次轉機、預算上限 追蹤狀態:(cost, node, constraint)
格子 二維矩陣題 四方向移動
多源 多個起點 一開始就把所有源頭放進去
雙向 大型圖 從兩端同時搜

常見模式與小技巧

優先佇列的狀態

python
# Basic state
(distance, node)

# With constraints
(cost, node, stops_remaining)

# Grid problems
(cost, row, col)

# With path tracking
(distance, node, path)

走訪集合的最佳化

python
# Option 1: Check after pop (recommended)
if node in visited:
    continue
visited.add(node)

# Option 2: Check distance
if d > dist[node]:
    continue

路徑還原

python
parent = {}
# During relaxation:
parent[v] = u

# Reconstruct path:
path = []
while node != source:
    path.append(node)
    node = parent[node]
path.reverse()

解題步驟

  1. 辨認圖的結構:邊是明給的,還是隱式的(格子)?
  2. 檢查限制:權重非負嗎?是單源嗎?
  3. 選模板:基本、帶限制、格子,還是多源?
  4. 定義狀態:優先佇列裡要追蹤什麼?
  5. 實作鬆弛:距離要怎麼更新?
  6. 處理終止條件:什麼時候停?要回傳什麼值?

相似 LeetCode 題目參考

格子類題目

LC # 標題 移動方向 關鍵特性 主要解法 其他解法 需要 dist[][]?
64 Minimum Path Sum 只有 ↓→ 可加成本 二維 DP 一維 DP、Dijkstra(殺雞用牛刀) ❌ 不用
1631 Path With Minimum Effort 四方向 單步最大差值(不可加) Dijkstra 二分搜尋、併查集 ✅ 要
778 Swim in Rising Water 四方向 格子值取最大 Dijkstra 併查集 ✅ 要
1263 Minimum Moves to Move Box 四方向 推箱子機制 Dijkstra + 狀態 - ✅ 要
882 Reachable Nodes In Subdivided Graph 圖 節點細分 Dijkstra - ✅ 要

LC 1631 深入解析:

  • 現有解法:四種主要作法(Dijkstra dist[]、Dijkstra visited、二分搜尋、併查集)
  • 最常見:Dijkstra 搭配 dist[][] 或 visited[] 陣列
  • 關鍵洞見:成本模型是 Math.max(effort, step_diff),不是加總 —— 這讓 DP 不可行
  • 參考檔:leetcode_java/src/main/java/LeetCodeJava/Graph/PathWithMinimumEffort.java(V0-V4.3)

經典最短路徑題目

LC # 標題 類型 關鍵特性
743 Network Delay Time 圖 廣播延遲
787 Cheapest Flights K Stops 圖 K 次轉機限制
1514 Path with Maximum Probability 圖 最大化機率
1928 Minimum Cost to Reach Destination 加權圖 K 個中繼點

多源最短路徑

LC # 標題 關鍵特性
1162 As Far from Land as Possible 多源 BFS-Dijkstra
2812 Find the Safest Path 格子上的多源
2290 Minimum Obstacle Removal 0-1 BFS 變形

主要實作檔案

  • Java 參考:leetcode_java/src/main/java/LeetCodeJava/DynamicProgramming/MinimumPathSum.java
    • V0:Dijkstra 搭配 dist[][](能過,但殺雞用牛刀)
    • V0-0-1、V1、V2:純 DP 作法(LC 64 的最佳解)
  • Python 參考:leetcode_python/Dynamic_Programming/minimum-path-sum.py
    • V0-1、V0-2:Dijkstra(最小堆積 + cost_grid[][])—— 見 7) LC 64
    • V1、V2:原地 DP O(1) 空間/一維滾動列(LC 64 的最佳解)

常見錯誤與提醒

🚫 常見錯誤:

  • 忘了檢查是否已走訪
  • 對負權重硬套 Dijkstra
  • 沒用優先佇列(用了普通佇列)
  • 帶限制的題目狀態比較寫錯
  • 沒處理不連通的分量

✅ 最佳實務:

  • 優先佇列一律用最小堆積
  • 記錄走訪過的節點,避免重複處理
  • 除了源點,其他距離一律初始化成無限大
  • 想清楚該用距離陣列還是走訪集合
  • 處理好邊界情況(空圖、無路可達)

面試提示

  1. 先問清楚限制:一定要問有沒有負權重
  2. 主動講複雜度:一開始就把時間/空間複雜度說出來
  3. 解釋鬆弛:更新距離是整個演算法的核心概念
  4. 提到替代方案:說明什麼時候 BFS 或 Bellman-Ford 更適合
  5. 需要就再最佳化:大型圖可以聊聊雙向搜尋

相關主題

  • BFS:無權重的最短路徑
  • Bellman-Ford:能處理負權重(詳細比較見上)
  • Floyd-Warshall:全點對最短路徑(詳細比較見上)
  • A 演算法*:由啟發函數引導的搜尋
  • SPFA:以佇列最佳化的 Bellman-Ford 變形
  • Johnson’s Algorithm:用重新賦權技巧解全點對