BFS — 進階變形
範圍 — 第一輪學習可以先跳過的 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 模式):
/**
* 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
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?
❌ 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 ✅
關鍵實作細節:
-
初始化策略:
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) -
更新條件:
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 -
為什麼第一次拜訪就是最短距離:
textBFS 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
/**
* 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
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
這個模式為什麼有效:
- 完整涵蓋:DFS 保證找到整座第一島,而不是只有一部分
- 最佳距離:從島上所有格子出發的多源 BFS 保證得到最短路徑
- 不做重複工:每一格在 DFS 中最多拜訪一次、在 BFS 中最多再一次
- 天然分層:BFS 的層數就對應橋的長度
模式特性:
- DFS 階段:最壞 O(m × n) — 標記整座第一島
- BFS 階段:最壞 O(m × n) — 擴張到整個格子
- 總時間:O(m × n) — 每一格只被拜訪常數次
- 空間:O(m × n) — 遞迴堆疊 + 佇列 + visited 陣列
何時使用此模式:
- 找兩個分離連通塊之間的最短連接
- 其中一個連通塊必須先被完整辨識出來,才能計算距離
- 題目需要從某個區域的整個邊界向外擴張
- 格子中剛好有兩個不同的區域/島嶼
主要變化:
- 只用邊界的 BFS:只把島的邊界格加入佇列(最佳化)
- 雙向 BFS:同時從兩座島向外擴張(更快)
- 改動原格子:直接在原格子上標記已拜訪(節省空間)
- 不同的標記值:依題目需求使用不同的值(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):到最近陸地的距離
// 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):到所有建築物的距離總和
// 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 陣列?」
答案:
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 + ...
每棟建築物都必須能獨立「看見」每一個空格,才能把自己的距離貢獻進去。
常見錯誤範例
// ❌ 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 陣列,不如直接改動格子本身:
// 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
}
格子技巧怎麼運作:
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
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 ✓
為什麼這裡一定要回溯:
❌ 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 等)
- ✅ 想在狀態空間中找最短路徑
關鍵實作細節:
-
入佇列前先標記:在加入佇列「之前」就放進 visited 集合
- 避免重複處理
- 確保時間複雜度是 O(state_space)
-
內層迴圈結束後還原:在某個位置試完所有變化後把狀態還原
- 確保下一個位置從正確的基準狀態出發
- 這就是「回溯」的部分
-
有效率地產生狀態:用 char 陣列修改,而不是字串串接
- 重複使用同一個陣列物件
- 只在需要時才重建字串
- 比 substring 操作快很多
-
提早結束:在出佇列時檢查是否為目標(而不是修改後才檢查)
- 找到目標可以立刻回傳
- 省下不必要的探索
與其他模式的比較:
| 模式 | 狀態修改 | 還原 | 適用情境 |
|---|---|---|---|
| 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
/**
* 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
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 集合?
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,而不是在站牌上?
❌ 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
/**
* 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
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 ✓
這個模式為什麼有效:
-
BFS 階段 — 建圖:
- 逐層遍歷保證第一次到達即為最短距離
Map<String, List<String>> parents記錄最短距離下的「所有」前驅Set<String> levelVisited允許同一層有多個父節點- 找到 endWord 後就停止(確保圖中只有最短路徑)
-
DFS 階段 — 列舉路徑:
- 從 endWord 往回走到 beginWord
- 在每個節點遞迴地探索所有前驅
- 這樣就能產生所有合法的最短路徑組合
- 回溯以探索其他路徑
-
避免重複與 TLE:
- BFS 只記錄最短距離
- DFS 只走最短路徑構成的 DAG
- 不會探索多餘或較長的路徑
- 圖的結構是最精簡的
關鍵實作細節:
| 細節 | 為什麼重要 | 少了它會怎樣 |
|---|---|---|
levelVisited 與 visited 分開 |
允許同一層存在多個父節點 | 漏掉合法的最短路徑 |
整層處理完才更新 visited |
記錄同層的所有前驅 | 錯誤地跳過合法的父節點 |
| 找到 endWord 後停止 BFS | 避免把較長路徑記進去 | 混入非最佳路徑 |
| 用 Map 存前驅 | 記錄所有前驅(不只一個) | 只找到部分路徑,而非全部 |
| DFS 反向遍歷 | 可以沿著多條前驅鏈往回走 | 無法列舉所有組合 |
模式特性:
- 兩階段演算法:先 BFS 再 DFS(依序進行,不是同時)
- 建圖:BFS 過程中建立一張前驅的反向 DAG
- 路徑列舉:用帶回溯的 DFS 走遍 DAG 中所有路徑
- 距離追蹤:用來判定最短距離並終止 BFS,不可或缺
- 多重父節點:一個節點在同一距離下可以有多個前驅
何時使用此模式:
- ✅ 要找出「所有」最短路徑(不只一條)
- ✅ 存在多條長度相同的最短路徑
- ✅ 需要列舉所有組合
- ✅ 必須避免探索較長路徑(防 TLE)
- ✅ 單字轉換、圖遍歷類題目
何時「不要」用:
- ❌ 只需要一條最短路徑(用 Pattern 7 或更單純的 BFS)
- ❌ 保證最短路徑唯一(複雜度是白花的)
- ❌ 要找最長路徑或所有路徑(單用 DFS 即可)
主要變化:
- 距離表變體:明確地存下距離(見程式碼中的 V0-3)
- 提早終止:一到達 endWord 就立刻停止 BFS(目前的做法)
- 雙向 BFS:從兩端同時擴張以縮小搜尋空間
- 預先算好鄰居:事先算出所有合法鄰居,避免重複產生(最佳化)
相似題目:
- 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 不受遞迴深度限制,而且天然地依群組順序產生組合。當群組內部的選擇之間存在交叉限制(例如路徑中不能有重複字元)時,回溯比較適合。
佇列如何演進(笛卡兒積視覺化)
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)
// 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)
// 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 直接從層數免費得到最小性,而且一次就回傳該長度下的所有答案。
兩條讓它不爆炸的規則:
- 用
visited集合去重 —"(())"可以由很多不同的刪除順序到達。 - 只要該層找到一個合法字串就停止擴張 — 該層剩下的仍要掃完(可能有好幾個答案),但絕不再建第
k+1層。
// 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
# 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 的鍵。只有在「抵達某格之後,未來的一切都相同」時這才正確。一旦你在行走途中攜帶了某樣東西——收集到的鑰匙、剩餘的消除次數、燃料、顏色、奇偶性——兩次抵達同一格就是搜尋中不同的位置,而修正方式永遠是同一行:
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
// 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
# 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
// 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
# 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 的葉節點配對數)為例:
- 做一次 DFS/遍歷,(a) 收集所有葉節點、(b) 建出無向鄰接表。
- 從每片葉子跑一次有界 BFS,只在
dist < distance時繼續擴張。每到達的另一片葉子就是一組好配對。 - 每一組
A–B會被找到兩次(從A一次、從B一次)→ 最後把總數除以 2。
b. 模式
# 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
// 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
# 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
// 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
# 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
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)
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
/**
* 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
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、移除障礙)
- 提早終止:只要有任一目標不可達就立刻回傳
- 最佳化:大型格子改用 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。