BFS — Advanced Variants
Scope — The BFS techniques a first pass should skip: bidirectional BFS, 0-1 BFS with a deque, multi-source beyond the canonical template, BFS over implicit state spaces, and all-shortest-path DAG enumeration — the must-know queue templates stay in the main sheet. See also: bfs.md — the canonical templates these variants build on, and where
Pattern 1-5 / 7 / 11 / 13references point; bfs_examples.md — the worked-solution archive; Dijkstra.md — once edges carry arbitrary weights.
LeetCode Problem Lists
Overview
Everything here is a variant of a template in bfs.md: the queue payload changes, the queue becomes a deque, the graph becomes implicit, or one BFS becomes many. Pattern numbering is shared across the three BFS sheets, so Pattern 4 / Pattern 11 below refer to the main sheet.
| Variant | Push / payload rule | Cost |
|---|---|---|
Multi-source (canonical, see bfs.md Pattern 4) |
all sources seeded at level 0 | O(V + E) |
| Independent BFS runs (Pattern 4.6) | fresh visited per source |
O(k(V + E)) |
| 0-1 BFS (Pattern 15) | deque: cost-0 front, cost-1 back | O(V + E) |
| Bidirectional | expand the smaller frontier | ~O(b^(d/2)) |
| Value-carrying (Pattern 14) | (node, valueSoFar) |
O(V + E) per query |
| Priority (Dijkstra-like) | heap ordered by distance | O(E log V) |
Multi-Source BFS — Deep Dive
Initialization Strategies and Execution Trace
Java Implementation (LC 542 - 01 Matrix Pattern):
/**
* 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;
}
Concrete Example: 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]]
Multi-Source BFS Distance Calculation (LC 542 Pattern)
The Problem Type: Calculate shortest distance from each cell to ANY source cell in a grid.
Why Multi-Source 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 ✅
Key Implementation Details:
-
Initialization Strategy:
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) -
The Update Condition:
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 -
Why First Visit = Shortest Distance:
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.
Pattern Recognition - Use Multi-Source BFS When:
- Need distance from each cell to ANY source (not a specific source)
- Multiple sources exist naturally in the problem
- Problem asks for “nearest/closest” among multiple options
- Can “flip” the problem (start from targets instead of sources)
Similar Problems Using This Pattern:
- LC 542: 01 Matrix (distance to nearest 0)
- LC 1162: As Far from Land as Possible (distance to nearest land)
- LC 286: Walls and Gates (distance from gates to rooms)
- LC 994: Rotting Oranges (time for all oranges to rot)
- LC 1765: Map of Highest Peak (assign heights with constraints)
Pattern 4.5: DFS + Multi-Source BFS (Island Expansion) — 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);
}
Concrete Example: 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
Why This Pattern Works:
- Complete Coverage: DFS ensures we find the entire first island, not just part of it
- Optimal Distance: Multi-source BFS from all island cells guarantees shortest path
- No Redundant Work: Each cell visited at most once in DFS + once in BFS
- Natural Layering: BFS level corresponds to bridge length
Pattern Characteristics:
- DFS Phase: O(m × n) worst case - mark entire first island
- BFS Phase: O(m × n) worst case - expand to entire grid
- Total Time: O(m × n) - each cell visited constant times
- Space: O(m × n) - recursion stack + queue + visited array
When to Use This Pattern:
- Find shortest connection between two separate components
- One component needs complete identification before distance calculation
- Problem requires expanding from entire boundary of a region
- Grid has exactly two distinct regions/islands
Key Variations:
- Boundary-Only BFS: Only add island boundary cells to queue (optimization)
- Bidirectional BFS: Expand from both islands simultaneously (faster)
- Modified Grid: Mark visited cells in original grid (space optimization)
- Different Marking: Use different values (2, -1) based on problem requirements
Similar Problems:
- LC 934: Shortest Bridge (connect two islands)
- LC 1162: As Far from Land as Possible (distance from any land cell)
- LC 542: 01 Matrix (distance to nearest 0 from each 1)
- LC 286: Walls and Gates (distance from gates to rooms)
- LC 1020: Number of Enclaves (count land cells not connected to boundary)
Pattern 4.6: Multi-Source BFS vs Independent BFS Runs (Critical Distinction)
🚨 IMPORTANT: This is the #1 source of confusion in multi-source BFS problems!
Many students confuse these two fundamentally different patterns:
Type 1: Simultaneous Multi-Source BFS (Patterns 4, 4.5)
- Goal: Find distance to the NEAREST source from each cell
- Setup: Add ALL sources to queue at
time = 0 - Visited: ONE shared
visitedarray/set for entire BFS - Logic: All sources expand simultaneously, layer by layer
- Result: Each cell knows its distance to the closest source
Example Problems:
- LC 542 (01 Matrix): Distance to nearest 0
- LC 994 (Rotting Oranges): Time for infection to spread
- LC 1162 (As Far from Land): Distance to nearest 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
}
}
Type 2: Independent BFS Runs (One BFS per source)
- Goal: Find SUM of distances or aggregate metric across ALL sources
- Setup: Run separate BFS for EACH source, one at a time
- Visited: FRESH
visitedarray for EACH BFS run - Logic: Each source independently explores the entire reachable space
- Result: Each cell accumulates distances/metrics from ALL sources
Example Problem:
- LC 317 (Shortest Distance from All Buildings): Sum of distances to 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});
}
}
}
}
}
Comparison Table
| Aspect | Simultaneous Multi-Source | Independent BFS Runs |
|---|---|---|
| Queue Init | Add ALL sources at once | Each source starts its own BFS |
| Visited Array | ONE shared across entire BFS | FRESH for each BFS run |
| Time Complexity | O(m×n) - single pass | O(k × m×n) where k = # sources |
| First Visit Means | Distance to NEAREST source | Distance from CURRENT source |
| Use Case | Find nearest/closest | Find sum/aggregate across all |
| Example | LC 542, 994, 1162 | LC 317 |
Why Fresh Visited Arrays in Independent BFS?
The Key Question: “Why can’t we reuse the visited array across different buildings in LC 317?”
The Answer:
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 + ...
Each building needs to “see” every empty cell independently to contribute its distance.
Common Mistake Example
// ❌ 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
}
Optimization: Grid Value Trick (Space-efficient alternative)
Instead of creating fresh boolean[][] visited arrays, modify the grid itself:
// 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
}
How Grid Trick Works:
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
Benefits:
- ✅ No need for
boolean[][] visitedarrays (saves space) - ✅ Automatically filters cells unreachable by earlier buildings
- ✅ Final value indicates how many buildings reached that cell
When to Use Which Pattern?
Use Simultaneous Multi-Source (Pattern 4) when:
- ✅ Need distance to nearest source
- ✅ Only care about the closest one
- ✅ Problem asks: “minimum distance to ANY…”
- ✅ Want O(m×n) time complexity
Use Independent BFS Runs (Pattern 4.6) when:
- ✅ Need sum of distances to all sources
- ✅ Need to know if cell is reachable from every source
- ✅ Problem asks: “find position that minimizes total distance…”
- ✅ Willing to accept O(k × m×n) time complexity
Quick Recognition Guide
| Problem Statement Contains… | Pattern to Use |
|---|---|
| “distance to nearest building” | Simultaneous Multi-Source |
| “sum of distances to all buildings” | Independent BFS Runs |
| “infection spreads from all sources” | Simultaneous Multi-Source |
| “all friends can reach in minimum total time” | Independent BFS Runs |
| “find the cell closest to any land” | Simultaneous Multi-Source |
State-Space & Implicit-Graph BFS
Pattern 7 Walkthrough: Why the Restore Step Matters — LC 127
The template itself is Pattern 7 in bfs.md; everything below dissects it.
Concrete Example: 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 ✓
Why Backtracking is Essential Here:
❌ 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
Pattern Characteristics:
- State Modification: In-place modification of mutable state (char array)
- Exploration: Try all possibilities at each “decision point” (position)
- Restoration: Undo changes before moving to next decision point
- BFS Integration: Process states level-by-level to find shortest path
- Visited Tracking: Prevent re-exploring same state (before enqueue)
When to Use This Pattern:
- ✅ Word transformation problems (Word Ladder, Word Ladder II)
- ✅ State space exploration where state can be modified in-place
- ✅ Need to try ALL neighbors systematically
- ✅ Neighbors differ by exactly ONE element (one char, one digit, one bit, etc.)
- ✅ Want to find shortest path through state space
Key Implementation Details:
-
Mark Before Enqueue: Add to visited set BEFORE adding to queue
- Prevents duplicate processing
- Ensures O(state_space) time complexity
-
Restore After Inner Loop: Restore state after trying all variations at one position
- Ensures correct base state for next position
- This is the “backtracking” aspect
-
Efficient State Creation: Use char array modification instead of string concatenation
- Reuse same array object
- Only recreate string when needed
- Much faster than substring operations
-
Early Exit: Check for target when dequeuing (not after modification)
- Allows immediate return when target found
- Saves unnecessary exploration
Comparison with Other Patterns:
| Pattern | State Modification | Restoration | Use Case |
|---|---|---|---|
| BFS + Backtracking | ✓ In-place | ✓ Required | Word transformations, state exploration |
| BFS + Queue Pairs | ✗ Create new | N/A | Simple shortest path without transformation |
| DFS + Backtracking | ✓ In-place | ✓ Required | All paths, permutations, combinations |
| Standard BFS | ✗ Create new | N/A | Graph traversal with pre-built adjacency |
Similar Problems:
- LC 127: Word Ladder (find shortest transformation sequence)
- LC 126: Word Ladder II (find ALL shortest transformation sequences - use DFS + backtracking instead)
- LC 752: Open the Lock (similar BFS pattern on digit combinations)
- LC 1008: Construct Binary Search Tree from Preorder Traversal (different pattern)
Pattern 8: BFS on Abstract Graph (Route-Level 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;
}
Concrete Example: 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 ✓
Why Two Visited Sets?
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)
Why BFS on Routes, Not Stops?
❌ 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
When to Use This Pattern:
- Minimum number of transfers/vehicles/connections
- Nodes in BFS are abstract entities (routes, lines, groups), not physical locations
- Problem involves shared stops/stations between routes
- Need to count transitions between groups, not individual steps
Similar Problems:
- LC 815: Bus Routes (minimum buses to reach target)
- LC 127: Word Ladder (can be seen as BFS on word groups — Pattern 7 is more natural)
- LC 841: Keys and Rooms (BFS/DFS on rooms accessed via keys)
- LC 1197: Minimum Knight Moves (BFS on chess positions)
Pattern 8.5: BFS + DFS (Find All Shortest Paths - DAG Enumeration) — 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);
}
Concrete Example: 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 ✓
Why This Pattern Works:
-
BFS Phase - Build the Graph:
- Level-order traversal ensures first reach = shortest distance
Map<String, List<String>> parentsrecords ALL predecessors at shortest distanceSet<String> levelVisitedallows multiple parents from same level- Stop after finding endWord (ensures only shortest paths in graph)
-
DFS Phase - Enumerate Paths:
- Walk backward from endWord to beginWord
- At each node, recursively explore all predecessors
- This generates ALL valid combinations of shortest paths
- Backtrack to explore alternative paths
-
Avoiding Duplicates & TLE:
- BFS only records shortest distances
- DFS only traverses the shortest-path DAG
- No redundant paths or longer paths explored
- Graph structure is minimal
Critical Implementation Details:
| Detail | Why Important | What Happens Without |
|---|---|---|
levelVisited separate from visited |
Allows multiple parents in same level | Lose valid shortest paths |
Update visited after level |
Records all same-level predecessors | Incorrectly skip valid parents |
| Stop BFS after finding endWord | Prevents longer paths from being recorded | Include suboptimal paths |
| Use Map for predecessors | Records all predecessors (not just one) | Find only some paths, not all |
| DFS backward traversal | Can follow multiple predecessor chains | Can’t enumerate all combinations |
Pattern Characteristics:
- Two-Phase Algorithm: BFS phase, then DFS phase (sequential, not simultaneous)
- Graph Construction: Build a reverse DAG of predecessors during BFS
- Path Enumeration: Use DFS with backtracking to traverse all paths in the DAG
- Distance Tracking: Essential for determining shortest distance and stopping BFS
- Multiple Parents: A node can have multiple predecessors at the same distance
When to Use This Pattern:
- ✅ Find ALL shortest paths (not just one)
- ✅ Multiple valid paths of same minimum length exist
- ✅ Need to enumerate all combinations
- ✅ Must avoid exploring longer paths (TLE prevention)
- ✅ Word transformation, graph traversal problems
When NOT to Use:
- ❌ Only need one shortest path (use Pattern 7 or simpler BFS)
- ❌ Unique shortest path guaranteed (unnecessary complexity)
- ❌ Need to find longest paths or all paths (use DFS alone)
Key Variations:
- Distance Map Variant: Store distances explicitly (see V0-3 in code)
- Early Termination: Stop BFS immediately upon reaching endWord (current approach)
- Bidirectional BFS: Expand from both ends to reduce search space
- Neighbor Precomputation: Pre-compute all valid neighbors to avoid regenerating (optimization)
Similar Problems:
- LC 126: Word Ladder II (find all shortest word transformation sequences)
- LC 913: Cat and Mouse (find all game strategies in shortest time)
- LC 1585: Check If String Is Transformable With Substring Sort Operations (enumerate transformations)
- LC 1948: Delete the Middle Node of a Linked List (not similar, but similar pattern in graph problems)
- LC 2115: Find All Recipes from Given Supplies (topological sort variant, similar enumeration pattern)
Comparison with Pattern 7 (BFS + Backtracking):
| Aspect | Pattern 7 (BFS + Backtracking) | Pattern 8.5 (BFS + DFS) |
|---|---|---|
| Goal | Find ONE shortest path | Find ALL shortest paths |
| Graph Building | On-the-fly neighbor generation | Explicit parent map construction |
| Visited Tracking | Standard visited set | levelVisited + visited (2-tier) |
| Enumeration | Early exit on found | DFS backtracks through all paths |
| Memory | O(M) for char array | O(N*M) for full parent graph |
| Example | LC 127 | LC 126 |
Pattern 9: BFS-Style Cartesian Product Generation (Level-by-Level Combination Building) — LC 1087
Core idea: Use a queue of partial strings (prefixes). Each independent “group” of options maps to one BFS depth level. For every level, drain the current queue and expand every prefix with every option in that group — producing the full Cartesian product one layer at a time.
This is not BFS over a graph with visited-node tracking. It is the BFS traversal structure applied to combination enumeration: process all nodes at depth k, generate all nodes at depth k+1, repeat.
When to Use
| Signal | Reason |
|---|---|
| Output must enumerate all combinations from independent choice groups | Cartesian product = one choice per group |
| Groups are independent (no constraint between them) | No pruning needed; every combination is valid |
| Want lexicographic order | Sort each group before BFS; row-major queue output is already sorted |
| Prefer iterative over recursive | BFS loop replaces DFS/backtracking recursion |
Why NOT DFS/backtracking? Both work, but BFS avoids recursion depth limits and naturally produces combinations in group-order. Backtracking is better when choices within groups have cross-constraints (e.g., no duplicate characters in path).
How the Queue Evolves (Cartesian Product Visualization)
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
Each level multiplies the queue size by the group’s option count.
Total combinations = |group_0| × |group_1| × ... × |group_k| (the Cartesian product size).
Template (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;
}
Key invariant: after processing group i, every string in the queue has length i + 1 (one char per group so far). The queue holds exactly the complete Cartesian product of groups [0..i].
Variant: Explicit State Object (more canonical 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));
}
Both variants are correct; the snapshot-size version is more concise; the State version makes the “BFS tree” structure explicit.
Comparison: BFS vs Backtracking for Cartesian Products
| Aspect | BFS (Pattern 9) | Backtracking / DFS |
|---|---|---|
| Control flow | Iterative loop, one group per iteration | Recursive, one group per call frame |
| Ordering | Natural row-major order if groups pre-sorted | Same if groups pre-sorted |
| Memory peak | Full final layer (all combinations) | O(depth) recursion stack |
| Pruning | Not straightforward | Easy to add |
| Constraint between groups? | Hard to express | Easy (check at each step) |
| Best for | Enumerate all, no cross-group constraints | Constrained search (e.g., sum ≤ target) |
Similar Problems
| Problem | LC # | How Cartesian BFS Applies |
|---|---|---|
| Brace Expansion | 1087 | Each {a,b} or single char = one group |
| Letter Combinations of a Phone Number | 17 | Each digit maps to a letter group |
| Letter Case Permutation | 784 | Each char has 1 (digit) or 2 (letter) options |
| Word Squares | 425 | Each position in the word is a group |
| Generalized Abbreviation | 320 | Each char = keep or abbreviate (2-option group) |
Rule of thumb: if you can parse the input into
kindependent groups and need all length-kstrings formed by picking one element from each group, use BFS-style Cartesian product generation. If groups have cross-constraints, switch to backtracking.
Pattern 12: BFS over String States — Stop at the First Fruitful Level — LC 301 Priority 5 of 5 — Must know — expect it in almost every loop
Key Idea: when the question is “remove the minimum number of X”, make one removal = one BFS level. Level k holds every string reachable by exactly k deletions. The first level that contains any valid string is the answer level — collect all valid strings on it and return immediately. No counting, no backtracking, no “how many to remove” pre-pass.
Why BFS beats DFS here: DFS finds some valid string but you’d still have to prove minimality; BFS gets minimality for free from the level number, and returns all answers of that length in one shot.
Two rules that keep it from exploding:
- Dedupe with a
visitedset —"(())"is reachable by many different deletion orders. - Stop expanding as soon as one valid string is found in the level — still finish scanning the rest of that level (there may be several answers), but never build level
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 [""]
Recognize this pattern when: “minimum number of removals/edits/changes to make X valid”, answer must list all optimal results, and the state is small enough to hash (a string).
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.
Tree → Undirected Graph BFS
Pattern 10: Tree → Undirected Graph + Per-Leaf Bounded BFS — LC 1530
a. Core idea
A tree only lets you walk down (parent → child). But the shortest path between two leaf nodes goes up to their lowest common ancestor and then down again — you need to traverse edges in both directions. So convert the tree into an undirected graph (add both parent→child and child→parent edges), then the leaf-to-leaf shortest path becomes a plain graph distance you can measure with BFS.
For LC 1530 (count pairs of leaves whose shortest path ≤ distance):
- One DFS/traversal to (a) collect all leaf nodes and (b) build the undirected adjacency map.
- Run a bounded BFS from each leaf, expanding only while
dist < distance. Every other leaf reached is a good pair. - Each pair
A–Bis discovered twice (once fromA, once fromB) → divide the final count by 2.
b. Pattern
# 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
Recognition signals
- Problem talks about the distance / shortest path between leaf (or arbitrary) nodes of a tree.
- Path must go up and then down → downward-only tree recursion is insufficient.
- Small constraints (
distance ≤ 10,N ≤ 2^10) make the bounded-BFS-per-leaf cost acceptable.
Alternative (often preferred): a single post-order DFS that returns a bucket array of leaf-distances and combines left/right subtrees at each node — O(N) with no graph. See DFS Pattern 15. Use BFS when the “convert-to-graph, then measure distance” mental model is clearer or when non-tree edges exist.
c. Similar LC
| Problem | LC # | Link to this pattern |
|---|---|---|
| Number of Good Leaf Nodes Pairs | 1530 | canonical tree→graph + per-leaf bounded BFS |
| All Nodes Distance K in Binary Tree | 863 | tree→graph, then BFS k steps from a target node — see Pattern 11 (cheaper: parent map only) |
| Amount of Time for Binary Tree to Be Infected | 2385 | tree→graph, BFS “infection spread” = max distance |
| Step-By-Step Directions From a Binary Tree Node | 2096 | shortest node-to-node path via LCA (up-then-down) |
| Closest Leaf in a Binary Tree | 742 | tree→graph, multi-source/target BFS to nearest leaf |
Weighted-Edge and Bidirectional BFS
Pattern 14: BFS Carrying an Accumulated Value Along the Path — LC 399 Priority 4 of 5 — High value — a gap here costs you rounds
Key Idea: the queue entry is (node, valueSoFar) instead of (node, distance). Every edge carries a weight and you combine it (multiply here, could be add/min/max) as you expand. BFS is still valid because the question is “is there a path, and what does it evaluate to” — not “the cheapest path”. In a/b = 2 the graph is a --2--> b and b --1/2--> a, so any path from x to y gives the same product and the first one BFS finds is fine.
Guard rails: return -1.0 if either endpoint was never seen in the equations (an unknown variable, not a disconnected one), and 1.0 for x/x only when x is known.
// 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]
Generalizes to: any “propagate a value along edges” question — swap the * for + (accumulate cost), min/max (bottleneck path), or a boolean (reachability). The queue payload is the only thing that changes.
Pattern 15: 0-1 BFS with a Deque — LC 1368 Priority 4 of 5 — High value — a gap here costs you rounds
Key Idea: when every edge costs 0 or 1, you don’t need Dijkstra’s heap. Use a deque:
- cost-0 edge →
addFirst(same “layer”, process before anything costlier) - cost-1 edge →
addLast(next layer)
The deque stays sorted by distance with at most two distinct values in it, so the first pop of a node is its final distance — Dijkstra’s guarantee at O(V + E) instead of O(E log V).
LC 1368: the grid tells you the “free” direction of each cell. Following the arrow costs 0; any other of the 4 moves costs 1 (one sign change).
// 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
| Edge weights | Structure | Push rule | Time |
|---|---|---|---|
| all 1 | Queue | always back | O(V + E) |
| 0 or 1 | Deque | 0 → front, 1 → back | O(V + E) |
| arbitrary ≥ 0 | PriorityQueue | by distance | O(E log V) |
Similar 0-1 BFS problems: LC 1263 Minimum Moves to Move a Box to Their Target Location (pushing the box costs 1, walking the player around costs 0 — state is (box, player)), and any “minimum obstacles to remove / minimum sign flips” grid question. Compare with LC 1730 (already in this doc) where every move costs 1 → plain queue is enough.
Bidirectional 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 with Priority (Dijkstra-like)
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
Repeated & Sequential BFS
Pattern 6: Sort + Repeated BFS (Sequential Shortest Paths) — 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
}
Concrete Example: 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
Pattern Characteristics:
- Sort Phase: O(k log k) where k = number of targets
- BFS Phase: O(k) iterations, each BFS is O(m×n) for grid
- Total Time: O(k log k + k×m×n) ≈ O(k×m×n) when k << m×n
- Space: O(m×n) for visited array (created fresh each BFS)
When to Use This Pattern:
- Must visit targets in specific order (sorted by value, priority, etc.)
- Need shortest path between consecutive targets
- Targets are sparse in the space
- Cannot use dynamic programming due to order constraints
Key Variations:
- Different Sort Criteria: Sort by distance, value, custom priority
- Modified Grid: Update grid after visiting target (set to 1, remove obstacle)
- Early Termination: Return immediately if any target unreachable
- Optimization: Use A* instead of BFS for large grids
Similar Problems:
- LC 675: Cut Off Trees for Golf Event (sort trees by height)
- LC 1293: Shortest Path with Obstacles Elimination (BFS with state)
- LC 864: Shortest Path to Get All Keys (BFS with key collection state)
- LC 1091: Shortest Path in Binary Matrix (basic BFS shortest path)
- LC 317: Shortest Distance from All Buildings (multi-source BFS)
Summary
| Signal in the problem | Variant | Section |
|---|---|---|
| “distance to the nearest source” vs “sum of distances to all sources” | one shared visited vs a fresh visited per source |
Pattern 4.6 |
| Two components, connect them with the fewest flips | DFS to mark one component, multi-source BFS out from all of it | Pattern 4.5 |
| “minimum transfers / buses / lines” | BFS whose nodes are routes, not stops | Pattern 8 |
| “return all shortest sequences” | BFS builds a predecessor DAG, DFS enumerates it | Pattern 8.5 |
| “minimum removals to make it valid” | one BFS level = one removal; stop at the first fruitful level | Pattern 12 |
| Enumerate every combination from independent groups | one group per BFS level (Cartesian product) | Pattern 9 |
| Path must go up a tree as well as down | convert the tree to an undirected graph, then BFS | Pattern 10 |
| Edges carry a value to accumulate (product / min / max) | queue payload is (node, valueSoFar) |
Pattern 14 |
| Every edge costs 0 or 1 | deque: cost-0 to the front, cost-1 to the back | Pattern 15 |
| Both endpoints are known and the path is long | expand the smaller frontier from each end | Bidirectional BFS |
| Edges carry arbitrary non-negative weights | stop using BFS — see Dijkstra.md | BFS with Priority |
| Visit targets in a forced order | sort, then one BFS per consecutive pair | Pattern 6 |
The must-know queue templates these build on are in bfs.md; the worked-solution archive is bfs_examples.md.