BFS — 進階變形

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

範圍 — 第一輪學習可以先跳過的 BFS 技巧:雙向 BFS、搭配 deque 的 0-1 BFS、超出標準模板的多源 BFS、在隱式狀態空間上的 BFS,以及 DAG 上所有最短路徑的列舉——必背的佇列模板留在主篇。 另見 — bfs.md — 這些變形所依據的標準模板,也是本文 Pattern 1-5 / 7 / 11 / 13 指向的地方;bfs_examples.md — 完整解題範例彙整;Dijkstra.md — 當邊帶有任意權重時改看這裡。

LeetCode 題目清單

總覽

這裡的每一項都是 bfs.md 中某個模板的變形:佇列裡放的內容改變、佇列換成 deque、圖變成隱式的,或者一次 BFS 變成多次 BFS。三份 BFS 文件共用同一套 Pattern 編號,所以下文的 Pattern 4 / Pattern 11 指的是主篇。

變形 推入/內容規則 成本
多源(標準版,見 bfs.md Pattern 4) 所有起點都放在 level 0 O(V + E)
各自獨立的 BFS(Pattern 4.6) 每個起點都用全新的 visited O(k(V + E))
0-1 BFS(Pattern 15) deque:成本 0 放前端、成本 1 放後端 O(V + E)
雙向 每次展開較小的那一側前緣 ~O(b^(d/2))
攜帶數值(Pattern 14) (node, valueSoFar) 每次查詢 O(V + E)
帶優先權(類 Dijkstra) 以距離排序的堆積(heap) O(E log V)

多源 BFS — 深入探討

初始化策略與執行追蹤

Java 實作(LC 542 - 01 Matrix 模式):

java
/**
 * Pattern: Multi-Source BFS for Distance Calculation
 * Use case: Calculate shortest distance from each cell to any source cell
 * Key insight: Start BFS from ALL sources simultaneously - first visit guarantees shortest path
 *
 * Time: O(m × n) - each cell visited at most once
 * Space: O(m × n) - queue can hold entire grid in worst case
 */
public int[][] multiSourceBFS(int[][] mat) {
    int rows = mat.length;
    int cols = mat[0].length;
    Queue<int[]> queue = new LinkedList<>();

    // Step 1: Initialize - Add all sources (0s) to queue, mark others as unvisited
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (mat[r][c] == 0) {
                queue.offer(new int[]{r, c});  // Multi-source starting points
            } else {
                // Mark as unvisited - two common approaches:
                // Option A: mat[r][c] = -1 (easier to check)
                // Option B: mat[r][c] = Integer.MAX_VALUE (easier for min comparison)
                mat[r][c] = -1;
            }
        }
    }

    int[][] dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}};

    // Step 2: BFS expansion from all sources
    while (!queue.isEmpty()) {
        int[] cur = queue.poll();
        int r = cur[0], c = cur[1];

        for (int[] d : dirs) {
            int nr = r + d[0];
            int nc = c + d[1];

            // Only process unvisited cells
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && mat[nr][nc] == -1) {
                // KEY: Distance = parent's distance + 1
                mat[nr][nc] = mat[r][c] + 1;
                queue.offer(new int[]{nr, nc});
            }
        }
    }

    return mat;
}

具體範例:LC 542 - 01 Matrix

text
Problem: Find distance to nearest 0 for each cell
Input:  [[0,0,0],     Output: [[0,0,0],
         [0,1,0],              [0,1,0],
         [1,1,1]]              [1,2,1]]

Execution trace:
Step 1 - Initialize:
  Queue: [(0,0), (0,1), (0,2), (1,0), (1,2)]  ← All 0s
  Grid:  [[0, 0, 0],
          [0, -1, 0],
          [-1, -1, -1]]

Step 2 - BFS Layer 1 (distance = 1):
  Process (0,0): Check (1,0) - already 0, skip
  Process (0,1): Check (1,1) - is -1, update to 1, enqueue
  Process (1,0): Check (2,0) - is -1, update to 1, enqueue

  Grid:  [[0, 0, 0],
          [0, 1, 0],
          [1, -1, -1]]
  Queue: [(1,1), (2,0), ...]

Step 3 - BFS Layer 2 (distance = 2):
  Process (1,1): Check (2,1) - is -1, update to 2, enqueue
  Process (2,0): Check (2,1) - is -1, update to 2, enqueue (redundant)

  Final: [[0, 0, 0],
          [0, 1, 0],
          [1, 2, 1]]

多源 BFS 的距離計算(LC 542 模式)

題型: 計算格子中每一格到「任一」來源格的最短距離。

為什麼要用多源 BFS?

text
❌ Naive Approach: Start BFS from each target cell
   - For each 1, run BFS to find nearest 0
   - Time: O(m×n) targets × O(m×n) BFS = O(m²×n²) ❌

✅ Multi-Source Approach: Start BFS from ALL sources simultaneously
   - Add all 0s to queue initially
   - Run single BFS that expands from all sources
   - Time: O(m×n) - each cell visited once ✅

關鍵實作細節:

  1. 初始化策略:

    java
    // Option A: Use sentinel value -1
    mat[r][c] = -1;  // Easier to check: if (mat[nr][nc] == -1)
    
    // Option B: Use MAX_VALUE
    mat[r][c] = Integer.MAX_VALUE;  // Easier for comparison: if (mat[nr][nc] > mat[r][c] + 1)
    
  2. 更新條件:

    java
    // Why only update when new distance is shorter?
    if (mat[nr][nc] > mat[r][c] + 1) {
        mat[nr][nc] = mat[r][c] + 1;
        queue.offer(new int[]{nr, nc});
    }
    
    // Explanation:
    // - In unweighted BFS, first visit = shortest path
    // - If cell already has distance ≤ current + 1, it has a better path
    // - Prevents redundant re-processing and ensures O(m×n) time
    
  3. 為什麼第一次拜訪就是最短距離:

    text
    BFS expands in layers (level-by-level):
    Layer 0: All sources (distance = 0)
    Layer 1: All cells 1 step away (distance = 1)
    Layer 2: All cells 2 steps away (distance = 2)
    ...
    
    When BFS first reaches a cell, it MUST be via the shortest path
    because all shorter paths were explored in earlier layers.
    

模式辨識 — 在以下情況使用多源 BFS:

  • 需要每一格到「任一」來源的距離(而不是某個特定來源)
  • 題目中天然存在多個來源
  • 題目問的是多個選項中的「最近/最接近」
  • 可以把問題「翻轉」過來(從目標出發,而不是從來源出發)

使用此模式的相似題目:

  • LC 542: 01 Matrix(到最近的 0 的距離)
  • LC 1162: As Far from Land as Possible(到最近陸地的距離)
  • LC 286: Walls and Gates(從門到各房間的距離)
  • LC 994: Rotting Oranges(所有橘子腐爛所需時間)
  • LC 1765: Map of Highest Peak(在限制下指派高度)

Pattern 4.5:DFS + 多源 BFS(島嶼擴張)— LC 934

java
/**
 * Pattern: DFS to identify first component, then Multi-Source BFS to find shortest distance to second component
 * Use case: Find shortest bridge between two islands, connect two separate regions
 * Key insight: DFS marks entire first island, BFS expands from ALL cells of first island simultaneously
 *
 * Time: O(m × n) - each cell visited at most once by DFS + once by BFS
 * Space: O(m × n) - queue can hold entire island boundary
 */
public int dfsMarkThenMultiSourceBFS(int[][] grid) {
    int n = grid.length;
    Queue<int[]> queue = new LinkedList<>();
    boolean found = false;

    // Step 1: DFS to find and mark first island (change 1 → 2)
    // Add ALL cells of first island to queue for multi-source BFS
    for (int y = 0; y < n && !found; y++) {
        for (int x = 0; x < n && !found; x++) {
            if (grid[y][x] == 1) {
                dfsMarkIsland(grid, x, y, queue);
                found = true;
            }
        }
    }

    // Step 2: Multi-Source BFS from entire first island
    // Expand outward layer by layer until reaching second island
    int[][] dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}};
    int steps = 0;
    boolean[][] visited = new boolean[n][n];

    while (!queue.isEmpty()) {
        int size = queue.size();

        for (int i = 0; i < size; i++) {
            int[] cur = queue.poll();
            int x = cur[0], y = cur[1];

            for (int[] d : dirs) {
                int nx = x + d[0];
                int ny = y + d[1];

                if (nx >= 0 && nx < n && ny >= 0 && ny < n && !visited[ny][nx]) {
                    visited[ny][nx] = true;

                    if (grid[ny][nx] == 1) {
                        return steps; // Reached second island
                    }

                    if (grid[ny][nx] == 0) {
                        queue.add(new int[]{nx, ny});
                    }
                }
            }
        }
        steps++;
    }

    return -1;
}

// DFS helper: Mark all cells of first island and add to queue
void dfsMarkIsland(int[][] grid, int x, int y, Queue<int[]> queue) {
    int n = grid.length;
    if (x < 0 || x >= n || y < 0 || y >= n || grid[y][x] != 1) {
        return;
    }

    grid[y][x] = 2; // Mark as visited (part of first island)
    queue.add(new int[]{x, y}); // Add to BFS queue

    // Recursively mark all connected cells
    dfsMarkIsland(grid, x + 1, y, queue);
    dfsMarkIsland(grid, x - 1, y, queue);
    dfsMarkIsland(grid, x, y + 1, queue);
    dfsMarkIsland(grid, x, y - 1, queue);
}

具體範例:LC 934 - Shortest Bridge

text
Problem: Connect two islands with minimum number of flips (0→1)
Grid: [[0,1],     Two islands: Island A at (0,1), Island B at (1,0)
       [1,0]]     Need to flip 1 cell to connect them

Step 1 - DFS marks Island A:
Original: [0,1]  →  After DFS: [0,2]  (2 = marked as first island)
          [1,0]                [1,0]
Queue: [(1,0)] - all cells of first island

Step 2 - BFS Layer 0 (from first island):
Check neighbors of (1,0):
- (0,0): water, add to queue → Queue: [(0,0)]
- (1,1): water, add to queue → Queue: [(0,0), (1,1)]
After Layer 0: steps = 0

Step 3 - BFS Layer 1:
Process (0,0):
  - (1,0): already visited (marked as 2)
  - (0,1): FOUND Island B (value = 1)! Return steps = 0

Result: 1 flip needed (but we count layers, answer may vary based on problem definition)

Key insight:
- DFS ensures we mark ENTIRE first island (not just one cell)
- Multi-source BFS expands from ALL boundary cells simultaneously
- This guarantees we find the absolute shortest bridge

這個模式為什麼有效:

  1. 完整涵蓋:DFS 保證找到整座第一島,而不是只有一部分
  2. 最佳距離:從島上所有格子出發的多源 BFS 保證得到最短路徑
  3. 不做重複工:每一格在 DFS 中最多拜訪一次、在 BFS 中最多再一次
  4. 天然分層:BFS 的層數就對應橋的長度

模式特性:

  • DFS 階段:最壞 O(m × n) — 標記整座第一島
  • BFS 階段:最壞 O(m × n) — 擴張到整個格子
  • 總時間:O(m × n) — 每一格只被拜訪常數次
  • 空間:O(m × n) — 遞迴堆疊 + 佇列 + visited 陣列

何時使用此模式:

  • 找兩個分離連通塊之間的最短連接
  • 其中一個連通塊必須先被完整辨識出來,才能計算距離
  • 題目需要從某個區域的整個邊界向外擴張
  • 格子中剛好有兩個不同的區域/島嶼

主要變化:

  1. 只用邊界的 BFS:只把島的邊界格加入佇列(最佳化)
  2. 雙向 BFS:同時從兩座島向外擴張(更快)
  3. 改動原格子:直接在原格子上標記已拜訪(節省空間)
  4. 不同的標記值:依題目需求使用不同的值(2、-1)

相似題目:

  • LC 934: Shortest Bridge(連接兩座島)
  • LC 1162: As Far from Land as Possible(到任一陸地格的距離)
  • LC 542: 01 Matrix(每個 1 到最近 0 的距離)
  • LC 286: Walls and Gates(從門到各房間的距離)
  • LC 1020: Number of Enclaves(統計未連到邊界的陸地格)

Pattern 4.6:多源 BFS vs 各自獨立的 BFS(關鍵區別)

🚨 重要:這是多源 BFS 題目中最容易搞混的第一名!

很多人會混淆這兩種本質完全不同的模式:

類型 1:同時進行的多源 BFS(Pattern 4、4.5)

  • 目標:找出每一格到最近來源的距離
  • 設定:在 time = 0 時把所有來源都放入佇列
  • visited:整個 BFS 共用一份 visited 陣列/集合
  • 邏輯:所有來源同時、一層一層地向外擴張
  • 結果:每一格得到的是它到最近來源的距離

範例題目:

  • LC 542 (01 Matrix):到最近 0 的距離
  • LC 994 (Rotting Oranges):感染擴散所需時間
  • LC 1162 (As Far from Land):到最近陸地的距離
java
// Simultaneous Multi-Source BFS Template
public int[][] simultaneousMultiSourceBFS(int[][] grid) {
    Queue<int[]> queue = new LinkedList<>();
    boolean[][] visited = new boolean[rows][cols];

    // Add ALL sources to queue at once
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] == SOURCE) {
                queue.offer(new int[]{r, c});
                visited[r][c] = true;  // ONE shared visited array
            }
        }
    }

    // Single BFS run - all sources expand together
    while (!queue.isEmpty()) {
        int[] cur = queue.poll();
        // Process neighbors...
        // First visit to any cell = shortest distance from ANY source
    }
}

類型 2:各自獨立的 BFS(每個來源跑一次 BFS)

  • 目標:求所有來源的距離總和或某種彙總指標
  • 設定:對每一個來源各自跑一次 BFS,一次一個
  • visited:每一次 BFS 都用全新的 visited 陣列
  • 邏輯:每個來源獨立地探索整個可達空間
  • 結果:每一格累積來自所有來源的距離/指標

範例題目:

  • LC 317 (Shortest Distance from All Buildings):到所有建築物的距離總和
java
// Independent BFS Runs Template - LC 317 Pattern
public int independentBFSRuns(int[][] grid) {
    int rows = grid.length;
    int cols = grid[0].length;

    // Global accumulator - each BFS adds to this
    int[][] totalDist = new int[rows][cols];
    int[][] reachCount = new int[rows][cols];

    int buildingCount = 0;

    // Run SEPARATE BFS for each source
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] == 1) {  // Found a building (source)
                buildingCount++;

                // FRESH visited array for this building's BFS
                boolean[][] visited = new boolean[rows][cols];

                bfsSingleSource(grid, r, c, visited, totalDist, reachCount);
            }
        }
    }

    // Find best cell that was reached by ALL buildings
    int minDist = Integer.MAX_VALUE;
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] == 0 && reachCount[r][c] == buildingCount) {
                minDist = Math.min(minDist, totalDist[r][c]);
            }
        }
    }

    return minDist == Integer.MAX_VALUE ? -1 : minDist;
}

// BFS from single source - accumulates distances
private void bfsSingleSource(int[][] grid, int sr, int sc,
                             boolean[][] visited,
                             int[][] totalDist,
                             int[][] reachCount) {
    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{sr, sc});
    visited[sr][sc] = true;

    int[][] dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    int dist = 0;

    while (!queue.isEmpty()) {
        int size = queue.size();
        dist++;

        for (int i = 0; i < size; i++) {
            int[] cur = queue.poll();
            int r = cur[0], c = cur[1];

            for (int[] d : dirs) {
                int nr = r + d[0];
                int nc = c + d[1];

                if (nr >= 0 && nr < grid.length && nc >= 0 && nc < grid[0].length
                    && !visited[nr][nc] && grid[nr][nc] == 0) {

                    visited[nr][nc] = true;

                    // Accumulate distance from this building
                    totalDist[nr][nc] += dist;
                    reachCount[nr][nc]++;

                    queue.offer(new int[]{nr, nc});
                }
            }
        }
    }
}

比較表

面向 同時進行的多源 BFS 各自獨立的 BFS
佇列初始化 一次放入所有來源 每個來源各自開始一次 BFS
visited 陣列 整個 BFS 共用一份 每次 BFS 都是全新的
時間複雜度 O(m×n) — 單趟掃過 O(k × m×n),k = 來源數
第一次拜訪代表 到最近來源的距離 到當前來源的距離
適用情境 找最近/最接近 求所有來源的總和/彙總
範例 LC 542、994、1162 LC 317

獨立 BFS 為什麼要用全新的 visited 陣列?

關鍵問題:「為什麼 LC 317 中不能在不同建築物之間重用 visited 陣列?」

答案:

text
Building A runs BFS:
  - Visits land cell (2,3) and marks it visited ✓
  - Calculates: distance from A to (2,3) = 5 steps

Building B runs BFS:
  - If we reuse visited array, cell (2,3) is still marked as visited!
  - We would SKIP (2,3) and never calculate distance from B to (2,3) ❌

But we NEED both distances because:
  - totalDist[2][3] = distFromA + distFromB + distFromC + ...

每棟建築物都必須能獨立「看見」每一個空格,才能把自己的距離貢獻進去。

常見錯誤範例

java
// ❌ WRONG - Reusing visited array
boolean[][] visited = new boolean[rows][cols];  // Created ONCE

for (Building b : allBuildings) {
    bfs(b, visited);  // All buildings share same visited array
    // Later buildings can't visit cells that earlier buildings marked!
}

// ✅ CORRECT - Fresh visited array
for (Building b : allBuildings) {
    boolean[][] visited = new boolean[rows][cols];  // Fresh each time
    bfs(b, visited);  // Each building can visit all reachable cells
}

最佳化:格子數值技巧(省空間的替代做法)

與其每次建立全新的 boolean[][] visited 陣列,不如直接改動格子本身:

java
// LC 317 Optimization: Decrement empty cells for each building
public int shortestDistance(int[][] grid) {
    int[][] totalDist = new int[rows][cols];
    int emptyValue = 0;  // Changes with each BFS: 0 → -1 → -2 → -3...
    int buildingCount = 0;

    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid[r][c] == 1) {
                buildingCount++;

                // BFS from this building, only visit cells with value = emptyValue
                bfsWithGridMarking(grid, r, c, emptyValue, totalDist);

                emptyValue--;  // Next building looks for different value
            }
        }
    }

    // Find best cell with value = (emptyValue + 1)
    // That cell was reached by ALL buildings
}

private void bfsWithGridMarking(int[][] grid, int sr, int sc,
                               int targetValue, int[][] totalDist) {
    // Only process cells with grid[r][c] == targetValue
    // After processing, change to (targetValue - 1)
    // This ensures cell must be reached by ALL previous buildings
}

格子技巧怎麼運作:

text
Initial grid: All empty cells = 0

Building 1 BFS:
  - Visit cells with value 0
  - Change them to -1 after visiting
  - Now empty cells = -1

Building 2 BFS:
  - Only visit cells with value -1
  - Change them to -2 after visiting
  - Now only cells reachable by BOTH buildings = -2

Building 3 BFS:
  - Only visit cells with value -2
  - Change them to -3
  - Only cells reachable by ALL 3 buildings = -3

好處:

  • ✅ 不需要 boolean[][] visited 陣列(省空間)
  • ✅ 自動濾掉先前建築物無法到達的格子
  • ✅ 最終數值直接代表有多少棟建築物到達過該格

該用哪一種模式?

在以下情況使用同時進行的多源 BFS(Pattern 4):

  • ✅ 需要到最近來源的距離
  • ✅ 只在乎最接近的那一個
  • ✅ 題目問:「到任一…的最小距離」
  • ✅ 想要 O(m×n) 的時間複雜度

在以下情況使用各自獨立的 BFS(Pattern 4.6):

  • ✅ 需要到所有來源距離的總和
  • ✅ 需要知道某格是否能被每一個來源到達
  • ✅ 題目問:「找出讓總距離最小的位置…」
  • ✅ 能接受 O(k × m×n) 的時間複雜度

快速辨識指南

題目敘述中出現… 該用的模式
「到最近建築物的距離」 同時進行的多源 BFS
「到所有建築物距離的總和」 各自獨立的 BFS
「感染從所有來源同時擴散」 同時進行的多源 BFS
「所有朋友能在最小總時間內抵達」 各自獨立的 BFS
「找出離任一陸地最近的格子」 同時進行的多源 BFS

狀態空間與隱式圖上的 BFS

Pattern 7 逐步解析:為什麼還原步驟很重要 — LC 127

模板本身是 bfs.md 中的 Pattern 7;以下全部是對它的拆解。

具體範例:LC 127 - Word Ladder

text
Problem: Transform "hit" → "cog" using dictionary ["hot","dot","dog","lot","log","cog"]
Expected: 5 (hit → hot → dot → dog → cog)

BFS + Backtracking Execution:

Layer 0: Queue = [hit], steps = 1
  Process "hit":
    Position 0: h→a,b,c,...,z  (none in dict)
    Position 1: i→a,b,c,...,o,... → "hot" ✓ add to queue
    Position 2: t→a,b,c,...,g,... (none in dict besides "hit" itself)
  After Layer 0: Queue = [hot]

Layer 1: Queue = [hot], steps = 2
  Process "hot":
    Position 0: h→a,b,c,...,d → "dot" ✓, "lot" ✓
    Position 1: o→... (backtrack, restore 'o')
    Position 2: t→... (none found)
  After Layer 1: Queue = [dot, lot]

Layer 2: Queue = [dot, lot], steps = 3
  Process "dot":
    Position 0: d→... (none found)
    Position 1: o→... (none found)
    Position 2: t→g → "dog" ✓
  Process "lot":
    Position 0: l→... (none found)
    Position 1: o→... (none found)
    Position 2: t→g → "log" ✓
  After Layer 2: Queue = [dog, log]

Layer 3: Queue = [dog, log], steps = 4
  Process "dog":
    Position 0: d→... (none found)
    Position 1: o→... (none found)
    Position 2: g→... (none found)
  Process "log":
    Position 0: l→... (none found)
    Position 1: o→... (none found)
    Position 2: g→c → "cog" ✓
  After Layer 3: Queue = [cog]

Layer 4: Queue = [cog], steps = 5
  Process "cog":
    cur.equals(endWord) == true
  RETURN steps = 5 ✓

為什麼這裡一定要回溯:

text
❌ Naive Approach (without backtracking):
   For each position, generate ONE new word per letter
   Problem: Must process all positions with CORRECT base state

✅ Backtracking Approach:
   1. Modify position 0 → try all 26 letters
   2. Restore position 0 to original
   3. Modify position 1 → try all 26 letters (with position 0 restored!)
   4. Restore position 1 to original
   5. Continue to position 2, etc.

   Result: Each position explored independently with correct base state

模式特性:

  • 狀態修改:對可變狀態(char 陣列)做原地修改
  • 探索:在每個「決策點」(位置)嘗試所有可能
  • 還原:在移到下一個決策點之前,把改動復原
  • 與 BFS 整合:逐層處理狀態以找到最短路徑
  • visited 追蹤:避免重複探索同一狀態(在入佇列前標記)

何時使用此模式:

  • ✅ 單字轉換題(Word Ladder、Word Ladder II)
  • ✅ 狀態可以原地修改的狀態空間探索
  • ✅ 需要有系統地嘗試「所有」鄰居
  • ✅ 鄰居之間剛好只差「一個」元素(一個字元、一個數字、一個 bit 等)
  • ✅ 想在狀態空間中找最短路徑

關鍵實作細節:

  1. 入佇列前先標記:在加入佇列「之前」就放進 visited 集合

    • 避免重複處理
    • 確保時間複雜度是 O(state_space)
  2. 內層迴圈結束後還原:在某個位置試完所有變化後把狀態還原

    • 確保下一個位置從正確的基準狀態出發
    • 這就是「回溯」的部分
  3. 有效率地產生狀態:用 char 陣列修改,而不是字串串接

    • 重複使用同一個陣列物件
    • 只在需要時才重建字串
    • 比 substring 操作快很多
  4. 提早結束:在出佇列時檢查是否為目標(而不是修改後才檢查)

    • 找到目標可以立刻回傳
    • 省下不必要的探索

與其他模式的比較:

模式 狀態修改 還原 適用情境
BFS + 回溯 ✓ 原地 ✓ 必要 單字轉換、狀態空間探索
BFS + 佇列存 pair ✗ 建立新的 不適用 不涉及轉換的單純最短路徑
DFS + 回溯 ✓ 原地 ✓ 必要 所有路徑、排列、組合
標準 BFS ✗ 建立新的 不適用 已建好鄰接表的圖遍歷

相似題目:

  • LC 127: Word Ladder(找最短轉換序列)
  • LC 126: Word Ladder II(找出「所有」最短轉換序列 — 改用 DFS + 回溯)
  • LC 752: Open the Lock(在數字組合上的類似 BFS 模式)
  • LC 1008: Construct Binary Search Tree from Preorder Traversal(不同的模式)

Pattern 8:抽象圖上的 BFS(以路線為節點)— LC 815

java
/**
 * Pattern: BFS where nodes are ROUTES (buses/lines), not physical locations
 * Use case: Find minimum number of transfers/buses to reach a destination
 * Key insight: Build stop→routes mapping, BFS on routes with two visited sets (buses + stops)
 *
 * Time: O(N * M) where N = number of routes, M = avg stops per route
 * Space: O(N * M) for the stop-to-routes map and visited sets
 */
public int routeLevelBFS(int[][] routes, int source, int target) {
    if (source == target) return 0;

    // Step 1: Build mapping from stop → list of route IDs
    Map<Integer, List<Integer>> stopToRoutes = new HashMap<>();
    for (int i = 0; i < routes.length; i++) {
        for (int stop : routes[i]) {
            stopToRoutes.computeIfAbsent(stop, k -> new ArrayList<>()).add(i);
        }
    }

    // Step 2: BFS on route IDs (not stops!)
    Queue<Integer> queue = new LinkedList<>();
    Set<Integer> visitedRoutes = new HashSet<>();
    Set<Integer> visitedStops = new HashSet<>();

    // Seed: all routes that pass through the source stop
    for (int routeId : stopToRoutes.getOrDefault(source, new ArrayList<>())) {
        queue.offer(routeId);
        visitedRoutes.add(routeId);
    }

    int busCount = 1; // Already on the first bus

    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            int currRoute = queue.poll();

            // Check all stops on this route
            for (int stop : routes[currRoute]) {
                if (stop == target) return busCount;

                if (visitedStops.contains(stop)) continue;
                visitedStops.add(stop);

                // Transfer: enqueue all OTHER routes at this stop
                for (int nextRoute : stopToRoutes.getOrDefault(stop, new ArrayList<>())) {
                    if (!visitedRoutes.contains(nextRoute)) {
                        visitedRoutes.add(nextRoute);
                        queue.offer(nextRoute);
                    }
                }
            }
        }
        busCount++;
    }

    return -1;
}

具體範例:LC 815 - Bus Routes

text
Problem: Find minimum buses to travel from source=1 to target=6
Routes: [[1,2,7], [3,6,7]]
  Route 0: stops 1→2→7→1→...
  Route 1: stops 3→6→7→3→...

Step 1 - Build stop→routes map:
  1 → [Route 0]
  2 → [Route 0]
  7 → [Route 0, Route 1]   ← transfer point!
  3 → [Route 1]
  6 → [Route 1]

Step 2 - BFS:
  Source stop = 1 → seed Route 0 into queue
  Queue: [Route 0], busCount = 1

  Layer 1 (busCount = 1):
    Process Route 0 → check stops [1, 2, 7]:
      Stop 1: not target. Routes at stop 1 = [Route 0] (already visited)
      Stop 2: not target. Routes at stop 2 = [Route 0] (already visited)
      Stop 7: not target. Routes at stop 7 = [Route 0, Route 1]
        → Route 1 not visited → enqueue Route 1
    Queue: [Route 1]

  busCount++ → busCount = 2

  Layer 2 (busCount = 2):
    Process Route 1 → check stops [3, 6, 7]:
      Stop 3: not target
      Stop 6: == target! → return busCount = 2 ✓

為什麼需要兩個 visited 集合?

text
visitedRoutes: Prevents boarding the same bus twice (infinite loop)
visitedStops:  Prevents re-processing transfer points
               (stop 7 connects Routes 0 and 1, but once explored, no need to revisit)

Without visitedStops: Every stop would re-check all its routes
  → Redundant work, potentially O(N²*M) instead of O(N*M)

為什麼在路線上做 BFS,而不是在站牌上?

text
❌ BFS on stops: Queue = [stop1, stop2, ...]
   Problem: How do you define "neighbors" of a stop?
   All other stops on the SAME route → huge adjacency list
   Loses the concept of "how many buses taken"

✅ BFS on routes: Queue = [route0, route1, ...]
   Each BFS layer = one bus ride
   Transfer = finding a new route at a shared stop
   busCount directly maps to BFS depth

何時使用此模式:

  • 求最少的轉乘/車輛/連線次數
  • BFS 中的節點是抽象實體(路線、線路、群組),而不是實體位置
  • 題目牽涉到路線之間共用的站點/車站
  • 要數的是群組之間的轉換次數,而不是單步移動數

相似題目:

  • LC 815: Bus Routes(到達目標所需的最少公車數)
  • LC 127: Word Ladder(可以視為在單字群組上做 BFS — 不過 Pattern 7 更自然)
  • LC 841: Keys and Rooms(透過鑰匙進入房間的 BFS/DFS)
  • LC 1197: Minimum Knight Moves(在西洋棋位置上的 BFS)

Pattern 8.5:BFS + DFS(找出所有最短路徑 — DAG 列舉)— LC 126

java
/**
 * Pattern: BFS to build shortest-path DAG, then DFS to enumerate all paths
 * Use case: Find ALL shortest transformation sequences (not just one)
 * Key insight: BFS builds a reverse graph of predecessors, DFS reconstructs all valid paths
 *
 * Time: O(N * M * 26 + paths) where N=words, M=length, paths=output size
 * Space: O(N * M) for graph + O(M) for DFS recursion stack
 */
public List<List<String>> findAllShortestPaths(String beginWord, String endWord, List<String> wordList) {
    List<List<String>> result = new ArrayList<>();
    Set<String> wordSet = new HashSet<>(wordList);

    if (!wordSet.contains(endWord))
        return result;

    // Map to store: word → list of predecessors (parents) at shortest distance
    Map<String, List<String>> parents = new HashMap<>();

    // Map to store: word → shortest distance from beginWord
    Map<String, Integer> distances = new HashMap<>();

    // ========== PHASE 1: BFS to build shortest-path DAG ==========
    Queue<String> queue = new LinkedList<>();
    queue.add(beginWord);
    distances.put(beginWord, 0);

    boolean found = false;
    String alpha = "abcdefghijklmnopqrstuvwxyz";

    while (!queue.isEmpty() && !found) {
        int size = queue.size();

        /**
         * CRITICAL: Use levelVisited to allow multiple parents at same distance
         *
         * Why separate from main visited set?
         * - Allows a word to be reached from multiple neighbors in same level
         * - We record ALL parents that reach it in shortest distance
         * - Main visited updated AFTER processing entire level
         *
         * Without this, we'd lose valid shortest paths!
         */
        Set<String> levelVisited = new HashSet<>();

        for (int i = 0; i < size; i++) {
            String word = queue.poll();
            char[] chars = word.toCharArray();

            for (int j = 0; j < chars.length; j++) {
                char original = chars[j];

                for (char c : alpha.toCharArray()) {
                    if (c == original)
                        continue;

                    chars[j] = c;
                    String nextWord = new String(chars);

                    // Skip words not in dictionary
                    if (!wordSet.contains(nextWord))
                        continue;

                    int newDistance = distances.get(word) + 1;

                    /**
                     * KEY LOGIC: Record ALL predecessors at shortest distance
                     *
                     * Case 1: First time reaching nextWord
                     * - Set distance
                     * - Add current word as first predecessor
                     * - Enqueue for next level
                     *
                     * Case 2: Reaching nextWord again at SAME distance (same level)
                     * - Add current word as ANOTHER predecessor
                     * - Don't enqueue again (already enqueued in this level)
                     *
                     * Case 3: Reaching nextWord at LONGER distance
                     * - Ignore (we only want shortest paths)
                     */
                    if (!distances.containsKey(nextWord)) {
                        // Case 1: First time reaching this word
                        distances.put(nextWord, newDistance);
                        parents.computeIfAbsent(nextWord, k -> new ArrayList<>()).add(word);

                        if (!levelVisited.contains(nextWord)) {
                            levelVisited.add(nextWord);
                            queue.add(nextWord);
                        }

                        if (nextWord.equals(endWord)) {
                            found = true;
                        }
                    } else if (distances.get(nextWord) == newDistance) {
                        // Case 2: Same distance from another parent
                        parents.computeIfAbsent(nextWord, k -> new ArrayList<>()).add(word);
                    }
                    // Case 3: Longer distance - ignore
                }

                chars[j] = original;  // Restore after exploring all letters
            }
        }
    }

    // ========== PHASE 2: DFS to enumerate all paths ==========
    if (distances.containsKey(endWord)) {
        List<String> path = new LinkedList<>();
        dfsEnumeratePaths(endWord, beginWord, parents, path, result);
    }

    return result;
}

/**
 * DFS backtracking to reconstruct all paths from endWord to beginWord
 *
 * Why backward (from endWord to beginWord)?
 * - parents map stores: word → predecessors
 * - Easier to traverse backward from target to source
 * - Build path in reverse, then it's already correct order when we reach beginWord
 */
private void dfsEnumeratePaths(String current, String beginWord,
                               Map<String, List<String>> parents,
                               List<String> path, List<List<String>> result) {
    // Add current word to path (building backward)
    path.add(0, current);

    // Base case: reached the beginning
    if (current.equals(beginWord)) {
        result.add(new ArrayList<>(path));
    } else {
        // Recursive case: explore all predecessors
        List<String> predecessors = parents.get(current);
        if (predecessors != null) {
            for (String prev : predecessors) {
                dfsEnumeratePaths(prev, beginWord, parents, path, result);
            }
        }
    }

    // Backtrack: remove current word before returning
    path.remove(0);
}

具體範例:LC 126 - Word Ladder II

text
Problem: Find ALL shortest paths from "hit" to "cog"
Dictionary: ["hot","dot","dog","lot","log","cog"]
Expected: [["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"]]

========== BFS PHASE ==========

Level 0: Queue = [hit], distances = {hit:0}
  Process "hit":
    Neighbors: "hot" (only one in dict differing by 1 letter)
    distances[hot] = 1, parents[hot] = [hit]
    levelVisited = {hot}
  After level: visited = {hit, hot}

Level 1: Queue = [hot], distances = {hit:0, hot:1}
  Process "hot":
    Neighbors: "dot", "lot", "hit" (hit already visited at distance 0, skip)
    distances[dot] = 2, parents[dot] = [hot]
    distances[lot] = 2, parents[lot] = [hot]
    levelVisited = {dot, lot}
  After level: visited = {hit, hot, dot, lot}

Level 2: Queue = [dot, lot], distances = {hit:0, hot:1, dot:2, lot:2}
  Process "dot":
    Neighbors: "dog", "hot" (hot at distance 1, skip)
    distances[dog] = 3, parents[dog] = [dot]
  Process "lot":
    Neighbors: "log", "hot" (hot at distance 1, skip)
    distances[log] = 3, parents[log] = [lot]
    levelVisited = {dog, log}
  After level: visited = {hit, hot, dot, lot, dog, log}

Level 3: Queue = [dog, log], distances = {hit:0, hot:1, dot:2, lot:2, dog:3, log:3}
  Process "dog":
    Neighbors: "cog", "dot" (dot at distance 2, skip)
    distances[cog] = 4, parents[cog] = [dog]
    found = true
  Process "log":
    Neighbors: "cog", "lot" (lot at distance 2, skip)
    cog already has distance 4, same as current+1!
    parents[cog] = [dog, log]  ← KEY: multiple parents!
  After level: visited = {hit, hot, dot, lot, dog, log, cog}

STOP BFS (found = true after finishing level)

Final parents map:
  cog → [dog, log]
  dog → [dot]
  log → [lot]
  dot → [hot]
  lot → [hot]
  hot → [hit]

========== DFS PHASE ==========

DFS from "cog" to "hit":

dfs(cog):
  path = [cog]
  predecessors = [dog, log]

  dfs(dog):
    path = [dog, cog]
    predecessors = [dot]

    dfs(dot):
      path = [dot, dog, cog]
      predecessors = [hot]

      dfs(hot):
        path = [hot, dot, dog, cog]
        predecessors = [hit]

        dfs(hit):
          path = [hit, hot, dot, dog, cog]
          hit == beginWord → FOUND PATH!
          result = [[hit, hot, dot, dog, cog]]

  dfs(log):
    path = [log, cog]
    predecessors = [lot]

    dfs(lot):
      path = [lot, log, cog]
      predecessors = [hot]

      dfs(hot):
        path = [hot, lot, log, cog]
        predecessors = [hit]

        dfs(hit):
          path = [hit, hot, lot, log, cog]
          hit == beginWord → FOUND PATH!
          result = [[hit, hot, dot, dog, cog], [hit, hot, lot, log, cog]]

Final result: 2 paths found ✓

這個模式為什麼有效:

  1. BFS 階段 — 建圖:

    • 逐層遍歷保證第一次到達即為最短距離
    • Map<String, List<String>> parents 記錄最短距離下的「所有」前驅
    • Set<String> levelVisited 允許同一層有多個父節點
    • 找到 endWord 後就停止(確保圖中只有最短路徑)
  2. DFS 階段 — 列舉路徑:

    • 從 endWord 往回走到 beginWord
    • 在每個節點遞迴地探索所有前驅
    • 這樣就能產生所有合法的最短路徑組合
    • 回溯以探索其他路徑
  3. 避免重複與 TLE:

    • BFS 只記錄最短距離
    • DFS 只走最短路徑構成的 DAG
    • 不會探索多餘或較長的路徑
    • 圖的結構是最精簡的

關鍵實作細節:

細節 為什麼重要 少了它會怎樣
levelVisited 與 visited 分開 允許同一層存在多個父節點 漏掉合法的最短路徑
整層處理完才更新 visited 記錄同層的所有前驅 錯誤地跳過合法的父節點
找到 endWord 後停止 BFS 避免把較長路徑記進去 混入非最佳路徑
用 Map 存前驅 記錄所有前驅(不只一個) 只找到部分路徑,而非全部
DFS 反向遍歷 可以沿著多條前驅鏈往回走 無法列舉所有組合

模式特性:

  • 兩階段演算法:先 BFS 再 DFS(依序進行,不是同時)
  • 建圖:BFS 過程中建立一張前驅的反向 DAG
  • 路徑列舉:用帶回溯的 DFS 走遍 DAG 中所有路徑
  • 距離追蹤:用來判定最短距離並終止 BFS,不可或缺
  • 多重父節點:一個節點在同一距離下可以有多個前驅

何時使用此模式:

  • ✅ 要找出「所有」最短路徑(不只一條)
  • ✅ 存在多條長度相同的最短路徑
  • ✅ 需要列舉所有組合
  • ✅ 必須避免探索較長路徑(防 TLE)
  • ✅ 單字轉換、圖遍歷類題目

何時「不要」用:

  • ❌ 只需要一條最短路徑(用 Pattern 7 或更單純的 BFS)
  • ❌ 保證最短路徑唯一(複雜度是白花的)
  • ❌ 要找最長路徑或所有路徑(單用 DFS 即可)

主要變化:

  1. 距離表變體:明確地存下距離(見程式碼中的 V0-3)
  2. 提早終止:一到達 endWord 就立刻停止 BFS(目前的做法)
  3. 雙向 BFS:從兩端同時擴張以縮小搜尋空間
  4. 預先算好鄰居:事先算出所有合法鄰居,避免重複產生(最佳化)

相似題目:

  • LC 126: Word Ladder II(找出所有最短單字轉換序列)
  • LC 913: Cat and Mouse(找出最短時間內的所有博弈策略)
  • LC 1585: Check If String Is Transformable With Substring Sort Operations(列舉轉換)
  • LC 1948: Delete the Middle Node of a Linked List(題目本身不相似,但圖類問題中有類似模式)
  • LC 2115: Find All Recipes from Given Supplies(拓撲排序變形,列舉模式類似)

與 Pattern 7(BFS + 回溯)的比較:

面向 Pattern 7(BFS + 回溯) Pattern 8.5(BFS + DFS)
目標 找「一條」最短路徑 找「所有」最短路徑
建圖 邊走邊產生鄰居 明確建出父節點對照表
visited 追蹤 標準 visited 集合 levelVisited + visited(兩層)
列舉 找到就提早結束 DFS 回溯走遍所有路徑
記憶體 char 陣列的 O(M) 完整父節點圖的 O(N*M)
範例 LC 127 LC 126

Pattern 9:BFS 式的笛卡兒積生成(逐層組合建構)— LC 1087

核心想法: 用一個裝著部分字串(前綴)的佇列。每一個獨立的選項「群組」對應 BFS 的一層深度。每一層都把當前佇列清空,並用該群組的每個選項去擴充每個前綴——一層一層地產生完整的笛卡兒積。

這不是在圖上做 BFS,也沒有 visited 節點追蹤。它是把 BFS 的遍歷結構套用到組合列舉上:處理深度 k 的所有節點,產生深度 k+1 的所有節點,如此重複。

何時使用

訊號 原因
輸出必須列舉來自獨立選項群組的所有組合 笛卡兒積 = 每個群組挑一個
群組彼此獨立(群組之間沒有限制) 不需要剪枝;每個組合都合法
需要字典序 BFS 前先把每個群組排序;佇列的列優先輸出本身就有序
偏好迭代而非遞迴 BFS 迴圈取代 DFS/回溯的遞迴

為什麼不用 DFS/回溯? 兩者都可行,但 BFS 不受遞迴深度限制,而且天然地依群組順序產生組合。當群組內部的選擇之間存在交叉限制(例如路徑中不能有重複字元)時,回溯比較適合。

佇列如何演進(笛卡兒積視覺化)

text
Input: s = "{a,b}c{d,e}f"
Parsed groups: [["a","b"], ["c"], ["d","e"], ["f"]]

Start:
  queue = [""]

After group ["a","b"]  (level 1):
  Drain "" → append "a", "b"
  queue = ["a", "b"]

After group ["c"]      (level 2):
  Drain "a" → "ac"
  Drain "b" → "bc"
  queue = ["ac", "bc"]

After group ["d","e"]  (level 3):
  Drain "ac" → "acd", "ace"
  Drain "bc" → "bcd", "bce"
  queue = ["acd", "ace", "bcd", "bce"]

After group ["f"]      (level 4):
  queue = ["acdf", "acef", "bcdf", "bcef"]   ← final result

每一層都把佇列大小乘上該群組的選項數。
總組合數 = |group_0| × |group_1| × ... × |group_k|(笛卡兒積的大小)。

模板(Java)

java
// Pattern 9: BFS-Style Cartesian Product Generation
// Time: O(G * |result|) where G = number of groups, |result| = total combinations
// Space: O(|result|) for the queue at the final level
public String[] cartesianBFS(List<List<String>> groups) {
    Queue<String> queue = new LinkedList<>();
    queue.add("");  // seed: one empty prefix at depth 0

    for (List<String> group : groups) {
        int size = queue.size();  // snapshot current layer size
        for (int k = 0; k < size; k++) {
            String prefix = queue.poll();
            for (String option : group) {
                queue.add(prefix + option);  // expand: prefix × option
            }
        }
        // After the loop: queue holds exactly one layer deeper
    }

    String[] res = new String[queue.size()];
    int idx = 0;
    while (!queue.isEmpty()) res[idx++] = queue.poll();
    return res;
}

關鍵不變量: 處理完群組 i 之後,佇列中每個字串的長度都是 i + 1(到目前為止每個群組貢獻一個字元)。佇列裡剛好就是群組 [0..i] 的完整笛卡兒積。

變體:明確的狀態物件(更接近標準 BFS)

java
// Use State(prefix, groupIndex) so the BFS loop drives termination
Queue<State> queue = new LinkedList<>();
queue.add(new State("", 0));

while (!queue.isEmpty()) {
    State cur = queue.poll();
    if (cur.groupIndex == groups.size()) {
        result.add(cur.prefix);  // leaf: complete combination
        continue;
    }
    for (String opt : groups.get(cur.groupIndex))
        queue.add(new State(cur.prefix + opt, cur.groupIndex + 1));
}

兩種寫法都正確;用快照大小的版本比較精簡,而 State 版本讓「BFS 樹」的結構更明顯。

比較:笛卡兒積用 BFS 還是回溯

面向 BFS(Pattern 9) 回溯/DFS
控制流程 迭代迴圈,一次處理一個群組 遞迴,一個呼叫框架處理一個群組
順序 群組先排序的話自然是列優先順序 群組先排序的話也一樣
記憶體高峰 完整的最後一層(所有組合) O(depth) 的遞迴堆疊
剪枝 不直觀 容易加入
群組之間有限制? 難以表達 容易(每一步檢查)
最適合 列舉全部、群組之間無限制 有限制的搜尋(例如總和 ≤ target)

相似題目

題目 LC # 笛卡兒積 BFS 如何套用
Brace Expansion 1087 每個 {a,b} 或單一字元 = 一個群組
Letter Combinations of a Phone Number 17 每個數字對應一組字母
Letter Case Permutation 784 每個字元有 1 種(數字)或 2 種(字母)選項
Word Squares 425 單字中的每個位置是一個群組
Generalized Abbreviation 320 每個字元 = 保留或縮寫(2 選項群組)

經驗法則:如果你能把輸入拆成 k 個獨立群組,而且需要從每個群組各挑一個元素所組成的所有長度為 k 的字串,就用 BFS 式笛卡兒積生成。若群組之間有交叉限制,改用回溯。


Pattern 12:在字串狀態上做 BFS — 停在第一個有結果的層 — LC 301 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

核心想法:當題目問的是「移除最少幾個 X」時,就讓一次移除 = 一個 BFS 層。第 k 層裝的是恰好刪除 k 個字元所能到達的所有字串。第一個包含任何合法字串的層就是答案所在的層——把該層所有合法字串收集起來,立刻回傳。不用計數、不用回溯、也不用事先算「要移除幾個」。

為什麼這裡 BFS 勝過 DFS:DFS 能找到「某個」合法字串,但你還得證明它是最少的;BFS 直接從層數免費得到最小性,而且一次就回傳該長度下的所有答案。

兩條讓它不爆炸的規則:

  1. 用 visited 集合去重 — "(())" 可以由很多不同的刪除順序到達。
  2. 只要該層找到一個合法字串就停止擴張 — 該層剩下的仍要掃完(可能有好幾個答案),但絕不再建第 k+1 層。
java
// java
// LC 301 - Remove Invalid Parentheses
// time = O(2^n * n)  worst case every subset of chars; n per validity check
// space = O(2^n)     visited set + queue
// IDEA: 1 BFS level = 1 deletion. First level containing a valid string is the answer level.
public List<String> removeInvalidParentheses(String s) {
    List<String> res = new ArrayList<>();
    Set<String> visited = new HashSet<>();
    Queue<String> q = new LinkedList<>();
    q.offer(s);
    visited.add(s);
    boolean found = false;
    while (!q.isEmpty()) {
        int size = q.size();
        for (int i = 0; i < size; i++) {
            String cur = q.poll();
            if (isValid(cur)) { res.add(cur); found = true; }
            if (found) continue;               // drain this level, but stop expanding
            for (int j = 0; j < cur.length(); j++) {
                char c = cur.charAt(j);
                if (c != '(' && c != ')') continue;   // only parens are removable
                String next = cur.substring(0, j) + cur.substring(j + 1);
                if (visited.add(next)) q.offer(next); // add() returns false if dup
            }
        }
        if (found) return res;                 // this level is minimal -> done
    }
    return res;
}

private boolean isValid(String t) {
    int cnt = 0;
    for (char c : t.toCharArray()) {
        if (c == '(') cnt++;
        else if (c == ')' && --cnt < 0) return false;  // ')' before its '('
    }
    return cnt == 0;
}
python
# python
# LC 301 - Remove Invalid Parentheses
# time = O(2^n * n), space = O(2^n)
# IDEA: level = number of deletions; return the first level that has valid strings
def removeInvalidParentheses(s):
    def valid(t):
        cnt = 0
        for ch in t:
            if ch == '(':
                cnt += 1
            elif ch == ')':
                cnt -= 1
                if cnt < 0:
                    return False
        return cnt == 0

    level = {s}                       # a set IS the visited-dedup for this level
    while level:
        found = [t for t in level if valid(t)]
        if found:
            return found              # minimal deletions -> all answers of this size
        nxt = set()
        for t in level:
            for i, ch in enumerate(t):
                if ch in '()':        # letters are never removed
                    nxt.add(t[:i] + t[i + 1:])
        level = nxt
    return [""]

看到這些就辨識出此模式:「移除/編輯/修改最少幾次讓 X 合法」、答案必須列出所有最佳結果,而且狀態小到可以雜湊(一個字串)。


Pattern 16:在擴增狀態上做 BFS — (cell, resource) — LC 864 & LC 1293 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

核心概念:一般的網格 BFS 以格子作為 visited 的鍵。只有在「抵達某格之後,未來的一切都相同」時這才正確。一旦你在行走途中攜帶了某樣東西——收集到的鑰匙、剩餘的消除次數、燃料、顏色、奇偶性——兩次抵達同一格就是搜尋中不同的位置,而修正方式永遠是同一行:

text
visited on (r, c)              ->  visited on (r, c, resource)

其餘部分仍是教科書式的佇列 BFS,所以第一次彈出終點時,你拿到的依然是最短路徑。代價在於狀態數量:m * n * |resource|。

題目敘述中的訊號 額外的維度
「收集鑰匙,門需要對應的鑰匙」(LC 864) 鑰匙位元遮罩,2^k 種值
「最多可以移除 k 個障礙物」(LC 1293) 剩餘消除次數,k+1 種值
「最多 k 站/k 條邊」(LC 787) 目前已使用的邊數
「最多可以反轉一條邊」 一個 0/1 旗標
「移動在兩位玩家/兩種顏色之間交替」(LC 1129) 上一次使用的顏色

為什麼 k 必須是鍵的一部分,而不是 best[r][c] 純量 — 以更多剩餘預算抵達某格絕不會更差,所以當新的抵達擁有嚴格更多預算時,這一格就值得再訪。只以格子為鍵會把這次抵達丟掉,並在可解的網格上回報 -1。以 (cell, k) 為鍵永遠正確;best[r][c] = 見過的最大預算 是同一件事的壓縮版,也是常見的記憶體最佳化。

LC 864 — 資源是鑰匙的位元遮罩

小寫 a..f 是鑰匙,大寫 A..F 是鎖。k <= 6,所以整串鑰匙用 6 個位元就放得下,狀態空間為 m * n * 64。

java
// java
// LC 864 - Shortest Path to Get All Keys
// IDEA: BFS over (row, col, keyMask). A lock is passable only when its bit is already
//       in the mask; stepping on a key ORs its bit in. Answer = first pop with all keys.
// time = O(m*n*2^k), space = O(m*n*2^k)
public int shortestPathAllKeys(String[] grid) {
    int m = grid.length, n = grid[0].length();
    int startR = 0, startC = 0, allKeys = 0;
    for (int r = 0; r < m; r++) {
        for (int c = 0; c < n; c++) {
            char ch = grid[r].charAt(c);
            if (ch == '@') { startR = r; startC = c; }
            else if (ch >= 'a' && ch <= 'f') allKeys |= 1 << (ch - 'a');
        }
    }

    int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
    boolean[][][] seen = new boolean[m][n][1 << 6];   // NOTE !!! the mask is part of the key
    Queue<int[]> q = new LinkedList<>();
    q.offer(new int[]{startR, startC, 0});
    seen[startR][startC][0] = true;

    int steps = 0;
    while (!q.isEmpty()) {
        int size = q.size();
        for (int s = 0; s < size; s++) {
            int[] cur = q.poll();
            int r = cur[0], c = cur[1], mask = cur[2];
            if (mask == allKeys) return steps;

            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
                char ch = grid[nr].charAt(nc);
                if (ch == '#') continue;
                // a lock we have no key for is a wall
                if (ch >= 'A' && ch <= 'F' && (mask & (1 << (ch - 'A'))) == 0) continue;

                int nMask = mask;
                if (ch >= 'a' && ch <= 'f') nMask |= 1 << (ch - 'a');
                if (seen[nr][nc][nMask]) continue;
                seen[nr][nc][nMask] = true;
                q.offer(new int[]{nr, nc, nMask});
            }
        }
        steps++;
    }
    return -1;
}
python
# python
# LC 864 - Shortest Path to Get All Keys
# IDEA: same BFS, state = (r, c, keyMask); picking up a key moves you to a DIFFERENT
#       layer of the search space, which is why a cell can be visited up to 2^k times
# time = O(m*n*2^k), space = O(m*n*2^k)
from collections import deque

def shortestPathAllKeys(grid):
    m, n = len(grid), len(grid[0])
    all_keys = 0
    start = (0, 0)
    for r in range(m):
        for c in range(n):
            ch = grid[r][c]
            if ch == '@':
                start = (r, c)
            elif ch.islower():
                all_keys |= 1 << (ord(ch) - ord('a'))

    q = deque([(start[0], start[1], 0, 0)])       # r, c, mask, steps
    seen = {(start[0], start[1], 0)}
    while q:
        r, c, mask, steps = q.popleft()
        if mask == all_keys:
            return steps
        for dr, dc in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nr, nc = r + dr, c + dc
            if not (0 <= nr < m and 0 <= nc < n):
                continue
            ch = grid[nr][nc]
            if ch == '#':
                continue
            if ch.isupper() and not (mask >> (ord(ch) - ord('A'))) & 1:
                continue                          # locked door, no key -> a wall
            n_mask = mask | (1 << (ord(ch) - ord('a'))) if ch.islower() else mask
            if (nr, nc, n_mask) in seen:
                continue
            seen.add((nr, nc, n_mask))
            q.append((nr, nc, n_mask, steps + 1))
    return -1

LC 1293 — 資源是倒數計數

同樣的骨架,mask 變成「我還能移除的障礙物數」。有兩件事值得帶進面試:

  • 捷徑。 若 k >= m + n - 2,你可以直接剷平一路穿過去,答案就是曼哈頓距離 m + n - 2,這個分支以 O(1) 回傳。但它的價值不只是一個提早結束:過了這關,所有剩下的輸入都滿足 k < m + n - 2,所以上界是 O(m*n*(m+n)),而不會隨著無上限的 k 成長。
  • 用 best[r][c] 取代三維 visited。 在每格儲存曾見過的最大剩餘預算,並跳過任何沒有嚴格更好的抵達。答案相同,記憶體 O(m*n)。
java
// java
// LC 1293 - Shortest Path in a Grid with Obstacles Elimination
// IDEA: BFS over (row, col, k left). best[r][c] = most budget ever seen here; an arrival
//       with <= that budget can never do better, so drop it.
// time = O(m*n*k), space = O(m*n)
public int shortestPath(int[][] grid, int k) {
    int m = grid.length, n = grid[0].length;
    if (k >= m + n - 2) return m + n - 2;        // enough budget to walk the diagonal

    int[][] best = new int[m][n];
    for (int[] row : best) Arrays.fill(row, -1);
    int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};

    Queue<int[]> q = new LinkedList<>();
    q.offer(new int[]{0, 0, k});
    best[0][0] = k;

    int steps = 0;
    while (!q.isEmpty()) {
        int size = q.size();
        for (int s = 0; s < size; s++) {
            int[] cur = q.poll();
            int r = cur[0], c = cur[1], left = cur[2];
            if (r == m - 1 && c == n - 1) return steps;

            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
                int nLeft = left - grid[nr][nc];   // grid is 0/1, so this spends the budget
                /** NOTE !!! `<=` not `<` — an arrival with the same budget is a duplicate,
                 *  and it is arriving no earlier, so it can never win. */
                if (nLeft < 0 || nLeft <= best[nr][nc]) continue;
                best[nr][nc] = nLeft;
                q.offer(new int[]{nr, nc, nLeft});
            }
        }
        steps++;
    }
    return -1;
}
python
# python
# LC 1293 - Shortest Path in a Grid with Obstacles Elimination
# IDEA: BFS over (r, c, k remaining); prune with the best budget ever seen at a cell
# time = O(m*n*k), space = O(m*n)
from collections import deque

def shortestPath(grid, k):
    m, n = len(grid), len(grid[0])
    if k >= m + n - 2:
        return m + n - 2

    best = [[-1] * n for _ in range(m)]
    best[0][0] = k
    q = deque([(0, 0, k, 0)])                     # r, c, k left, steps
    while q:
        r, c, left, steps = q.popleft()
        if (r, c) == (m - 1, n - 1):
            return steps
        for dr, dc in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nr, nc = r + dr, c + dc
            if not (0 <= nr < m and 0 <= nc < n):
                continue
            n_left = left - grid[nr][nc]
            if n_left < 0 or n_left <= best[nr][nc]:
                continue                          # out of budget, or already been here richer
            best[nr][nc] = n_left
            q.append((nr, nc, n_left, steps + 1))
    return -1

常見錯誤

  • 在格子上標記 seen — LC 864 就會在「撿到鑰匙後必須穿越自己先前路線」時回報 -1,而測資大多是這種情況。
  • 把已經持有的鑰匙算成新狀態 — 無害但會讓佇列加倍;nMask == mask 反正會被 seen 檢查擋下。
  • 在 LC 1293 中,在進入的格子檢查預算,卻在離開的格子扣預算。單獨採用任一種慣例都正確 —— 兩個端點都是 0,所以一條路徑付費的障礙物相同 —— 但混用兩者,會讓一整串相鄰的障礙物只用 1 的預算就通過。

相似題目:LC 787 Cheapest Flights Within K Stops((node, stops);有權重,所以用 Dijkstra 或 Bellman-Ford)、LC 1928 Minimum Cost to Reach Destination in Time((node, time))、LC 1129 Shortest Path with Alternating Colors((node, lastColour))、LC 847 Shortest Path Visiting All Nodes((node, visitedMask) — 見 dp_bitmask.md)。當額外的維度讓邊變成有權重時,同樣的狀態改放進堆積而不是佇列 — Dijkstra.md。


樹 → 無向圖的 BFS

Pattern 10:樹 → 無向圖 + 每片葉子的有界 BFS — LC 1530

a. 核心想法

樹只允許你往下走(父 → 子)。但兩個葉節點之間的最短路徑會先往上走到它們的最低共同祖先,再往下走——你需要在兩個方向上都能走邊。所以把樹轉成無向圖(同時加入 parent→child 和 child→parent 兩種邊),葉到葉的最短路徑就變成單純的圖距離,用 BFS 就能量。

以 LC 1530(統計最短路徑 ≤ distance 的葉節點配對數)為例:

  1. 做一次 DFS/遍歷,(a) 收集所有葉節點、(b) 建出無向鄰接表。
  2. 從每片葉子跑一次有界 BFS,只在 dist < distance 時繼續擴張。每到達的另一片葉子就是一組好配對。
  3. 每一組 A–B 會被找到兩次(從 A 一次、從 B 一次)→ 最後把總數除以 2。

b. 模式

python
# python — Tree → Graph conversion + per-leaf bounded BFS (LC 1530)
# time  = O(L * (V + E)) = O(L * N)   L = #leaves, N = #nodes
# space = O(N)                        adjacency map + queue/visited
from collections import deque, defaultdict

class Solution:
    def countPairs(self, root, distance):
        leaves = []
        graph = defaultdict(list)

        # Step 1: collect leaves + build UNDIRECTED graph
        def build(node, parent=None):
            if not node:
                return
            if not node.left and not node.right:   # leaf
                leaves.append(node)
            if parent:                              # bidirectional edge
                graph[node].append(parent)
                graph[parent].append(node)
            build(node.left, node)
            build(node.right, node)
        build(root)

        cnt = 0
        # Step 2: bounded BFS from every leaf
        for leaf in leaves:
            queue = deque([(leaf, 0)])              # (node, dist)
            visited = {leaf}
            while queue:
                cur, d = queue.popleft()
                if cur != leaf and not cur.left and not cur.right:
                    cnt += 1                         # reached another leaf
                if d < distance:                     # only expand within limit
                    for nxt in graph[cur]:
                        if nxt not in visited:
                            visited.add(nxt)
                            queue.append((nxt, d + 1))
        return cnt // 2                              # each pair counted twice

辨識訊號

  • 題目談的是樹中葉節點(或任意節點)之間的距離/最短路徑。
  • 路徑必須先往上再往下 → 只能往下的樹遞迴不夠用。
  • 限制很小(distance ≤ 10、N ≤ 2^10),讓「每片葉子跑一次有界 BFS」的成本可以接受。

替代做法(通常更好): 用單次 後序 DFS,回傳一個葉距離的桶陣列,並在每個節點合併左右子樹——O(N) 而且完全不用建圖。見 DFS Pattern 15。當「先轉成圖、再量距離」這個心智模型比較清楚,或圖中存在非樹邊時,才用 BFS。

c. 相似 LC

題目 LC # 與此模式的關聯
Number of Good Leaf Nodes Pairs 1530 標準的樹→圖 + 每片葉子有界 BFS
All Nodes Distance K in Binary Tree 863 樹→圖,再從目標節點 BFS k 步 — 見 Pattern 11(更省:只建父節點表)
Amount of Time for Binary Tree to Be Infected 2385 樹→圖,BFS「感染擴散」= 最大距離
Step-By-Step Directions From a Binary Tree Node 2096 透過 LCA 的節點到節點最短路徑(先上後下)
Closest Leaf in a Binary Tree 742 樹→圖,用多源/多目標 BFS 找最近的葉子

帶權邊與雙向 BFS

Pattern 14:BFS 沿路徑攜帶累積值 — LC 399 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

核心想法:佇列中放的是 (node, valueSoFar) 而不是 (node, distance)。每條邊帶著一個權重,擴張時把它合併進去(這題是相乘,也可以是加法/min/max)。BFS 仍然成立,因為題目問的是「是否存在一條路徑,以及它算出來是多少」——而不是「最便宜的路徑」。在 a/b = 2 中圖是 a --2--> b 和 b --1/2--> a,所以從 x 到 y 的任何路徑乘積都相同,BFS 找到的第一條就可以用。

防呆條件:若任一端點從未在等式中出現過(未知變數,不是不連通),回傳 -1.0;而 x/x 只有在 x 已知時才回傳 1.0。

java
// java
// LC 399 - Evaluate Division
// time = O(Q * (V + E)), space = O(V + E)     Q = #queries
// IDEA: weighted graph a->b = v, b->a = 1/v; BFS carries the running product
public double[] calcEquation(List<List<String>> equations, double[] values,
                             List<List<String>> queries) {
    Map<String, Map<String, Double>> g = new HashMap<>();
    for (int i = 0; i < values.length; i++) {
        String a = equations.get(i).get(0), b = equations.get(i).get(1);
        g.computeIfAbsent(a, k -> new HashMap<>()).put(b, values[i]);
        g.computeIfAbsent(b, k -> new HashMap<>()).put(a, 1.0 / values[i]);
    }
    double[] res = new double[queries.size()];
    for (int i = 0; i < queries.size(); i++)
        res[i] = bfs(g, queries.get(i).get(0), queries.get(i).get(1));
    return res;
}

private double bfs(Map<String, Map<String, Double>> g, String src, String dst) {
    if (!g.containsKey(src) || !g.containsKey(dst)) return -1.0;  // unknown variable
    if (src.equals(dst)) return 1.0;
    Queue<Object[]> q = new LinkedList<>();
    Set<String> seen = new HashSet<>();
    q.offer(new Object[]{src, 1.0});
    seen.add(src);
    while (!q.isEmpty()) {
        Object[] cur = q.poll();
        String node = (String) cur[0];
        double prod = (double) cur[1];
        for (Map.Entry<String, Double> e : g.get(node).entrySet()) {
            if (e.getKey().equals(dst)) return prod * e.getValue();
            if (seen.add(e.getKey()))
                q.offer(new Object[]{e.getKey(), prod * e.getValue()});
        }
    }
    return -1.0;   // known variables, but no path connects them
}
python
# python
# LC 399 - Evaluate Division
# time = O(Q * (V + E)), space = O(V + E)
# IDEA: queue holds (node, product_so_far) instead of (node, distance)
from collections import deque, defaultdict

def calcEquation(equations, values, queries):
    g = defaultdict(dict)
    for (a, b), v in zip(equations, values):
        g[a][b] = v
        g[b][a] = 1.0 / v

    def bfs(src, dst):
        if src not in g or dst not in g:
            return -1.0                     # variable never appeared
        if src == dst:
            return 1.0
        q = deque([(src, 1.0)])
        seen = {src}
        while q:
            node, prod = q.popleft()
            for nxt, w in g[node].items():
                if nxt == dst:
                    return prod * w
                if nxt not in seen:
                    seen.add(nxt)
                    q.append((nxt, prod * w))
        return -1.0

    return [bfs(a, b) for a, b in queries]

可推廣到:任何「沿邊傳播某個值」的問題——把 * 換成 +(累積成本)、min/max(瓶頸路徑),或布林值(可達性)。唯一要改的只有佇列裡裝的東西。


Pattern 15:搭配 deque 的 0-1 BFS — LC 1368 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

核心想法:當每條邊的成本只有 0 或 1 時,不需要用 Dijkstra 的堆積(heap)。改用 deque:

  • 成本 0 的邊 → addFirst(同一「層」,比任何更貴的先處理)
  • 成本 1 的邊 → addLast(下一層)

deque 中的元素始終依距離有序,而且裡面最多只有兩種不同的距離值,所以一個節點第一次被取出時的距離就是最終距離——用 O(V + E) 拿到 Dijkstra 等級的保證,而不是 O(E log V)。

LC 1368:格子告訴你每一格的「免費」方向。順著箭頭走成本為 0;其餘 3 個方向各成本 1(改一個標誌)。

java
// java
// LC 1368 - Minimum Cost to Make at Least One Valid Path in a Grid
// time = O(m*n), space = O(m*n)
// IDEA: 0-1 BFS. Following grid[r][c]'s arrow costs 0 -> push FRONT; turning costs 1 -> push BACK.
public int minCost(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};    // index k <-> grid value k+1
    int[][] dist = new int[m][n];
    for (int[] row : dist) Arrays.fill(row, Integer.MAX_VALUE);
    dist[0][0] = 0;
    Deque<int[]> dq = new ArrayDeque<>();
    dq.offerFirst(new int[]{0, 0});
    while (!dq.isEmpty()) {
        int[] cur = dq.pollFirst();
        int r = cur[0], c = cur[1];
        for (int k = 0; k < 4; k++) {
            int nr = r + dirs[k][0], nc = c + dirs[k][1];
            if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
            int cost = (grid[r][c] == k + 1) ? 0 : 1;
            if (dist[r][c] + cost < dist[nr][nc]) {
                dist[nr][nc] = dist[r][c] + cost;
                if (cost == 0) dq.offerFirst(new int[]{nr, nc});   // 0-weight: front
                else           dq.offerLast(new int[]{nr, nc});    // 1-weight: back
            }
        }
    }
    return dist[m - 1][n - 1];
}
python
# python
# LC 1368 - Minimum Cost to Make at Least One Valid Path in a Grid
# time = O(m*n), space = O(m*n)
# IDEA: deque BFS - appendleft for 0-cost moves, append for 1-cost moves
def minCost(grid):
    m, n = len(grid), len(grid[0])
    dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]      # grid value 1,2,3,4
    INF = float('inf')
    dist = [[INF] * n for _ in range(m)]
    dist[0][0] = 0
    dq = deque([(0, 0)])
    while dq:
        r, c = dq.popleft()
        for k, (dr, dc) in enumerate(dirs):
            nr, nc = r + dr, c + dc
            if not (0 <= nr < m and 0 <= nc < n):
                continue
            cost = 0 if grid[r][c] == k + 1 else 1
            if dist[r][c] + cost < dist[nr][nc]:
                dist[nr][nc] = dist[r][c] + cost
                if cost == 0:
                    dq.appendleft((nr, nc))        # free move -> front
                else:
                    dq.append((nr, nc))            # paid move -> back
    return dist[m - 1][n - 1]

BFS vs 0-1 BFS vs Dijkstra

邊權重 資料結構 推入規則 時間
全為 1 佇列 一律放後端 O(V + E)
0 或 1 Deque 0 → 前端、1 → 後端 O(V + E)
任意 ≥ 0 PriorityQueue 依距離排序 O(E log V)

相似的 0-1 BFS 題目:LC 1263 Minimum Moves to Move a Box to Their Target Location(推箱子成本 1、玩家自己走動成本 0 — 狀態是 (box, player)),以及任何「最少移除幾個障礙/最少翻轉幾個標誌」的格子題。對照 LC 1730(本文已提過),那題每一步成本都是 1 → 用一般佇列就夠了。


雙向 BFS

python
def bidirectional_bfs(start, end):
    """Meet in the middle - faster for long paths"""
    if start == end:
        return 0
    
    forward = {start: 0}
    backward = {end: 0}
    queue_forward = deque([start])
    queue_backward = deque([end])
    
    while queue_forward or queue_backward:
        # Expand smaller frontier
        if len(forward) <= len(backward):
            if expand_level(queue_forward, forward, backward):
                return True
        else:
            if expand_level(queue_backward, backward, forward):
                return True
    
    return False

帶優先權的 BFS(類 Dijkstra)

python
import heapq

def weighted_bfs(start, end, graph):
    """BFS variant for weighted graphs"""
    heap = [(0, start)]
    distances = {start: 0}
    
    while heap:
        dist, node = heapq.heappop(heap)
        
        if node == end:
            return dist
            
        if dist > distances.get(node, float('inf')):
            continue
            
        for neighbor, weight in graph[node]:
            new_dist = dist + weight
            if new_dist < distances.get(neighbor, float('inf')):
                distances[neighbor] = new_dist
                heapq.heappush(heap, (new_dist, neighbor))
    
    return -1

重複與連續執行的 BFS

Pattern 6:排序 + 重複 BFS(連續最短路徑)— LC 675

java
/**
 * Pattern: Sort targets by priority, then repeatedly call BFS to find shortest paths
 * Use case: Visit multiple targets in specific order, minimize total travel distance
 * Key insight: BFS guarantees shortest path between each pair of consecutive targets
 *
 * Time: O(k × m × n) where k = number of targets, m×n = grid size
 * Space: O(m × n) for visited array in each BFS call
 */
public int sortAndBFS(List<List<Integer>> grid) {
    int rows = grid.size();
    int cols = grid.get(0).size();

    // Step 1: Collect all targets and sort by priority (e.g., value)
    List<int[]> targets = new ArrayList<>();
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (grid.get(r).get(c) > 1) {
                // Store [value, row, col]
                targets.add(new int[]{grid.get(r).get(c), r, c});
            }
        }
    }

    // Sort by value (ascending) - defines visit order
    targets.sort(Comparator.comparingInt(a -> a[0]));

    // Step 2: Sequentially visit each target using BFS
    int totalSteps = 0;
    int startR = 0, startC = 0; // Starting position

    for (int[] target : targets) {
        int targetR = target[1];
        int targetC = target[2];

        // Find shortest path from current position to next target
        int steps = bfs(grid, startR, startC, targetR, targetC);

        if (steps == -1) {
            return -1; // Target unreachable
        }

        totalSteps += steps;

        // Update starting position for next iteration
        startR = targetR;
        startC = targetC;
    }

    return totalSteps;
}

/**
 * Standard BFS to find shortest path in grid
 * Returns minimum steps from (sr, sc) to (tr, tc), or -1 if unreachable
 */
private int bfs(List<List<Integer>> grid, int sr, int sc, int tr, int tc) {
    if (sr == tr && sc == tc) return 0;

    int rows = grid.size();
    int cols = grid.get(0).size();

    Queue<int[]> queue = new LinkedList<>();
    queue.offer(new int[]{sr, sc});

    boolean[][] visited = new boolean[rows][cols];
    visited[sr][sc] = true;

    int[][] dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    int steps = 0;

    while (!queue.isEmpty()) {
        int size = queue.size();
        steps++;

        for (int i = 0; i < size; i++) {
            int[] cur = queue.poll();
            int r = cur[0], c = cur[1];

            for (int[] dir : dirs) {
                int nr = r + dir[0];
                int nc = c + dir[1];

                // Check bounds and obstacles
                if (nr < 0 || nr >= rows || nc < 0 || nc >= cols
                    || visited[nr][nc] || grid.get(nr).get(nc) == 0) {
                    continue;
                }

                // Found target
                if (nr == tr && nc == tc) {
                    return steps;
                }

                visited[nr][nc] = true;
                queue.offer(new int[]{nr, nc});
            }
        }
    }

    return -1; // Unreachable
}

具體範例:LC 675 - Cut Off Trees for Golf Event

text
Problem: Cut trees in forest from shortest to tallest, return minimum steps
Grid: [[1,2,3],    Trees: (0,1)=2, (0,2)=3, (1,2)=4, (2,0)=7, (2,1)=6, (2,2)=5
       [0,0,4],    Sorted: 2→3→4→5→6→7
       [7,6,5]]

Path: (0,0) →[1 step]→ (0,1) cut 2
      (0,1) →[2 steps]→ (0,2) cut 3
      (0,2) →[1 step]→ (1,2) cut 4
      (1,2) →[1 step]→ (2,2) cut 5
      (2,2) →[1 step]→ (2,1) cut 6
      (2,1) →[1 step]→ (2,0) cut 7
Total: 1+2+1+1+1+1 = 7 steps (Note: Problem statement has different expected output)

Key insight: Must cut in sorted order, BFS finds shortest path between each pair

模式特性:

  • 排序階段:O(k log k),k = 目標數量
  • BFS 階段:k 次迭代,格子上每次 BFS 為 O(m×n)
  • 總時間:O(k log k + k×m×n) ≈ 當 k << m×n 時約 O(k×m×n)
  • 空間:O(m×n) 的 visited 陣列(每次 BFS 都重新建立)

何時使用此模式:

  • 必須依特定順序拜訪目標(依數值、優先權等排序)
  • 需要相鄰兩個目標之間的最短路徑
  • 目標在空間中很稀疏
  • 因為有順序限制而無法使用動態規劃

主要變化:

  1. 不同的排序準則:依距離、數值或自訂優先權排序
  2. 改動格子:拜訪目標後更新格子(設為 1、移除障礙)
  3. 提早終止:只要有任一目標不可達就立刻回傳
  4. 最佳化:大型格子改用 A* 而非 BFS

相似題目:

  • LC 675: Cut Off Trees for Golf Event(依高度排序樹木)
  • LC 1293: Shortest Path with Obstacles Elimination(帶狀態的 BFS)
  • LC 864: Shortest Path to Get All Keys(帶鑰匙收集狀態的 BFS)
  • LC 1091: Shortest Path in Binary Matrix(基本的 BFS 最短路徑)
  • LC 317: Shortest Distance from All Buildings(多源 BFS)

總結

題目中的訊號 變形 章節
「到最近來源的距離」 vs 「到所有來源距離的總和」 共用一份 visited vs 每個來源一份全新的 visited Pattern 4.6
兩個連通塊,用最少的翻轉把它們連起來 DFS 標出其中一塊,再從整塊做多源 BFS 向外擴張 Pattern 4.5
「最少轉乘/公車/線路數」 節點是路線而非站牌的 BFS Pattern 8
「回傳所有最短序列」 BFS 建出前驅 DAG,再用 DFS 列舉 Pattern 8.5
「最少移除幾個才能變成合法」 一個 BFS 層 = 一次移除;停在第一個有結果的層 Pattern 12
列舉獨立群組的每一種組合 一個群組對應一個 BFS 層(笛卡兒積) Pattern 9
路徑在樹中必須能往上也能往下 把樹轉成無向圖,再做 BFS Pattern 10
邊上帶著要累積的值(乘積/min/max) 佇列裡裝 (node, valueSoFar) Pattern 14
每條邊成本都是 0 或 1 deque:成本 0 放前端、成本 1 放後端 Pattern 15
兩個端點都已知,而且路徑很長 從兩端各自擴張較小的那一側前緣 雙向 BFS
邊帶著任意非負權重 別再用 BFS — 見 Dijkstra.md 帶優先權的 BFS
依強制順序拜訪目標 先排序,再對每對相鄰目標各跑一次 BFS Pattern 6

這些變形所依據的必背佇列模板在 bfs.md;完整解題範例彙整在 bfs_examples.md。