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 Priority 5 of 5 — Must know — expect it in almost every loop
核心想法:當題目問的是「移除最少幾個 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 over an Augmented State — (cell, resource) — LC 864 & LC 1293 Priority 5 of 5 — Must know — expect it in almost every loop
Key Idea: plain grid BFS keys visited on the cell. That is only correct when arriving at a
cell makes every future identical. The moment you carry something along the walk — keys collected,
eliminations left, fuel, a colour, a parity — two arrivals at the same cell are different
positions in the search, and the fix is always the same one line:
visited on (r, c) -> visited on (r, c, resource)
Everything else stays a textbook queue BFS, so the first time you pop the goal you still have the
shortest path. The cost is the state count: m * n * |resource|.
| Signal in the statement | The extra dimension |
|---|---|
| “collect keys, doors need the matching key” (LC 864) | key bitmask, 2^k values |
“you may remove at most k obstacles” (LC 1293) |
eliminations remaining, k+1 values |
“at most k stops / edges” (LC 787) |
edges used so far |
| “you may reverse at most one edge” | a 0/1 flag |
| “moves alternate between two players / colours” (LC 1129) | last colour used |
Why
kmust be part of the key, not abest[r][c]scalar — reaching a cell with more budget left is never worse, so a cell is worth revisiting when the new arrival has strictly more budget. Keying only on the cell throws that arrival away and reports-1on grids that are solvable. Keying on(cell, k)is always correct;best[r][c] = max budget seenis the same thing compressed, and is the usual memory optimisation.
LC 864 — the resource is a bitmask of keys
Lowercase a..f are keys, uppercase A..F are locks. k <= 6, so the whole key ring fits in 6
bits and the state space is 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 — the resource is a countdown
Same skeleton, mask becomes “obstacles I may still remove”. Two things are worth carrying into
the interview:
- The shortcut. If
k >= m + n - 2you can bulldoze straight through, so the answer is the Manhattan distancem + n - 2and that branch returns inO(1). It is worth more than one early exit, though: past it every surviving input hask < m + n - 2, so the bound isO(m*n*(m+n))rather than growing with an unboundedk. best[r][c]instead of a 3-Dvisited. Store the largest remaining budget ever seen at a cell and skip any arrival that is not strictly better. Same answers,O(m*n)memory.
// 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
Common mistakes
- Marking
seenon the cell — LC 864 then reports-1whenever the path must cross its own earlier route after picking a key up, which is most of the test set. - Counting a key you already hold as a new state — harmless but doubles the queue;
nMask == maskis caught by theseencheck anyway. - In LC 1293, spending budget on the cell you leave instead of the one you enter. The start
cell is guaranteed to be
0, so both happen to pass the sample and diverge on the real tests.
Similar problems: LC 787 Cheapest Flights Within K Stops ((node, stops); weighted, so
Dijkstra or 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) — see dp_bitmask.md). When the extra dimension
makes edges weighted, the same state goes into a heap instead of a queue —
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 Priority 4 of 5 — High value — a gap here costs you rounds
核心想法:佇列中放的是 (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 Priority 4 of 5 — High value — a gap here costs you rounds
核心想法:當每條邊的成本只有 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。