Bfs
Last updated: Jul 7, 2026Table of Contents
- Overview
- Key Properties
- Core Characteristics
- Fundamental Concepts
- 1. Node States (for cycle detection)
- 2. BFS vs DFS Comparison
- Implementation Patterns
- Pattern 1: Basic Tree BFS — LC 102
- Pattern 2: Level-by-Level BFS — LC 102
- Pattern 3: Graph BFS with Visited Set — LC 200
- Pattern 4: Multi-Source BFS (Distance Calculation) — LC 542
- Pattern 4.5: DFS + Multi-Source BFS (Island Expansion) — LC 934
- Pattern 4.6: Multi-Source BFS vs Independent BFS Runs (Critical Distinction)
- Pattern 5: BFS with Path Tracking — LC 127
- Pattern 6: Sort + Repeated BFS (Sequential Shortest Paths) — LC 675
- Pattern 7: BFS + Backtracking (State Space Exploration) — LC 127
- Pattern 8: BFS on Abstract Graph (Route-Level BFS) — LC 815
- Pattern 8: BFS + DFS (Find All Shortest Paths - DAG Enumeration) — LC 126
- Pattern 9: BFS-Style Cartesian Product Generation (Level-by-Level Combination Building) — LC 1087
- Pattern 10: Tree → Undirected Graph + Per-Leaf Bounded BFS — LC 1530
- Problem Categories
- 1. Tree Traversal Problems
- 2. Shortest Path Problems
- 3. Graph Structure Problems
- 4. Matrix/Grid Problems
- 5. Combination Enumeration Problems (Pattern 9 — BFS-Style Cartesian Product)
- Time & Space Complexity
- BFS Time Complexity Analysis
- Common Mistakes & Tips
- ❌ Common Mistakes
- ✅ Best Practices
- When to Update Grid Status & Count (Mark Before vs After Enqueue)
- When to Increment Time/Distance: Beginning vs End of BFS Level
- Advanced Techniques
- Bidirectional BFS
- BFS with Priority (Dijkstra-like)
- Core Concepts Summary
- Multi-Source BFS Distance Calculation (LC 542 Pattern)
- Quick Reference
- When to Use BFS
- BFS vs Dijkstra — When to Use Which
- When NOT to Use BFS
- Key LeetCode Problems
- LC Examples
- 2-1) Rotting Oranges (LC 994) — Multi-source BFS
- 2-2) Word Ladder (LC 127) — BFS Shortest Transformation
- 2-3) Word Ladder II (LC 126) — BFS + DFS All Shortest Paths
- 2-4) Shortest Path in Binary Matrix (LC 1091) — BFS Shortest Path
- 2-4) 01 Matrix (LC 542) — Multi-source BFS from All Zeros
- 2-5) Open the Lock (LC 752) — BFS on State Space
- 2-6) Surrounded Regions (LC 130) — BFS from Border
- 2-7) Course Schedule (LC 207) — BFS Topological Sort (Kahn’s)
- 2-8) Walls and Gates (LC 286) — Multi-source BFS
- 2-9) Minimum Height Trees (LC 310) — BFS Leaf Trimming
- 2-10) Snakes and Ladders (LC 909) — BFS on Board
- 2-11) Bus Routes (LC 815) — Route-Level BFS
- 2-12) Pacific Atlantic Water Flow (LC 417) — BFS from Both Oceans
- 2-13) Perfect Squares (LC 279) — BFS on Abstract Graph (Number Decomposition)
- 2-14) Closest Leaf in a Binary Tree (LC 742) — Tree → Graph + BFS ⭐⭐⭐⭐
- 2-15) Populating Next Right Pointers (LC 116 / 117) — Level BFS wires the next links ⭐⭐⭐⭐
BFS (Breadth-First Search)
Overview
Breadth-First Search is a graph traversal algorithm that explores nodes level by level, visiting all nodes at the current depth before moving to nodes at the next depth.
Key Properties
- Complete: Always finds a solution if one exists
- Optimal: Finds shortest path in
unweightedgraphs - Space Complex: O(b^d) where b=branching factor, d=depth
- Time Complex: O(V + E) for graphs, O(n) for trees
Core Characteristics
- Uses Queue data structure (FIFO - First In, First Out)
- Guarantees shortest path in unweighted graphs
- Explores nodes level by level (breadth first, then depth)
- Memory intensive compared to DFS
Fundamental Concepts
1. Node States (for cycle detection)
- State 0: Not visited (white)
- State 1: Currently being processed (gray)
- State 2: Completely processed (black)
2. BFS vs DFS Comparison
🔹 BFS (Breadth-First Search)
- Uses a Queue
- Order: FIFO (First In, First Out)
- How it works: Visit a node → Add all neighbors to queue → Process in order added
- 👉 Think: level by level traversal
🔹 DFS (Depth-First Search)
- Uses a Stack (explicitly or via recursion)
- Order: FILO / LIFO (Last In, First Out)
- How it works: Go as deep as possible along one path → Backtrack when needed
- 👉 Think: go deep first, then backtrack
| Aspect | BFS | DFS |
|---|---|---|
| Data Structure | Queue (FIFO) | Stack / Recursion (LIFO) |
| Traversal Order | Level by level | Deep path first, then backtrack |
| Memory | O(w) — width of tree | O(h) — height of tree |
| Shortest Path | ✅ Yes (unweighted) | ❌ No |
| Complete | ✅ Yes | ❌ No (infinite spaces) |
| When to Use | Shortest path, level traversal | Explore all paths, topological sort, cycle detection |
Implementation Patterns
Pattern 1: Basic Tree BFS — LC 102
from collections import deque
def bfs_tree(root):
if not root:
return []
queue = deque([root])
result = []
while queue:
node = queue.popleft()
result.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return result
Pattern 2: Level-by-Level BFS — LC 102
def bfs_levels(root):
if not root:
return []
queue = deque([root])
levels = []
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
levels.append(current_level)
return levels
Pattern 3: Graph BFS with Visited Set — LC 200
def bfs_graph(start, graph):
queue = deque([start])
visited = set([start])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return result
Pattern 4: Multi-Source BFS (Distance Calculation) — LC 542
def multi_source_bfs(grid, sources):
"""Start BFS from multiple sources simultaneously"""
queue = deque(sources) # All sources at once
visited = set(sources)
while queue:
x, y = queue.popleft()
for dx, dy in [(0,1), (0,-1), (1,0), (-1,0)]:
nx, ny = x + dx, y + dy
if (0 <= nx < len(grid) and 0 <= ny < len(grid[0])
and (nx, ny) not in visited):
visited.add((nx, ny))
queue.append((nx, ny))
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;
}
Alternative: Using MAX_VALUE for initialization (allows for optimization)
public int[][] multiSourceBFS_optimized(int[][] mat) {
int rows = mat.length;
int cols = mat[0].length;
Queue<int[]> queue = new LinkedList<>();
// Initialize with MAX_VALUE for non-sources
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});
} else {
mat[r][c] = Integer.MAX_VALUE; // Treat as infinity
}
}
}
int[][] dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int r = cur[0], c = cur[1];
for (int[] d : dirs) {
/** NOTE !!!
*
* - mat[r][c] + 1:
* This is the "new potential distance" you just calculated
* by walking from your current cell (r, c) to its neighbor (nr, nc).
*
*
* - mat[nr][nc]:
* This is the "best distance found so far" for that neighb
*
*/
int nr = r + d[0];
int nc = c + d[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
/**
* KEY OPTIMIZATION: Only update when new distance is shorter
* Why this works:
* - In unweighted BFS, first visit = shortest distance
* - If mat[nr][nc] <= mat[r][c] + 1, cell already has better/equal path
* - No need to re-process cells that won't improve
*
* This prevents redundant enqueuing and guarantees O(m×n) time
*/
/** e.g.
*
* "If the existing distance at the neighbor is worse than the one I just found, update the neighbor and put it in the queue."
*/
if (mat[nr][nc] > mat[r][c] + 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]]
Why This Pattern Works:
- Simultaneous Expansion: All sources expand at same rate → layer by layer
- First Visit = Shortest: In unweighted BFS, first arrival guarantees shortest path
- No Backtracking: Once a cell is visited, we’ve found its shortest distance
- Linear Time: Each cell visited exactly once → O(m×n) total
Key Insight - Why Start from 0s, Not 1s?
- ❌ Starting from each 1 → O(m×n) BFS calls → O(m²×n²) total time
- ✅ Starting from all 0s → Single BFS pass → O(m×n) total time
- Principle: Flip the problem - instead of “how far is this 1 from any 0?”, ask “how far can all 0s reach?”
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 |
Pattern 5: BFS with Path Tracking — LC 127
def bfs_with_path(start, target):
queue = deque([(start, [start])])
visited = {start}
while queue:
node, path = queue.popleft()
if node == target:
return path
for neighbor in get_neighbors(node):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None
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)
Pattern 7: BFS + Backtracking (State Space Exploration) — LC 127
/**
* Pattern: BFS + Backtracking for exploring transformations
* Use case: Word transformations, state space exploration where each state can transform to multiple neighbors
* Key insight: Modify state in-place, explore all neighbors, restore state before moving to next position
*
* Time: Depends on state space (e.g., O(N * M * 26) for word ladder where N=words, M=length)
* Space: O(N) for visited set, O(M) for char array
*/
public int bfsWithBacktracking(String beginWord, String endWord, List<String> wordList) {
Set<String> dict = new HashSet<>(wordList);
Set<String> visited = new HashSet<>();
Queue<String> q = new LinkedList<>();
q.add(beginWord);
visited.add(beginWord);
int steps = 1; // beginWord counts as step 1
String alpha = "abcdefghijklmnopqrstuvwxyz";
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
String cur = q.poll();
// Early exit when target found
if (cur.equals(endWord))
return steps;
// Convert to char array for efficient modification
char[] arr = cur.toCharArray();
/**
* Key Insight: Backtracking allows exploring ALL transformations
*
* For each position, we try ALL 26 letters:
* - Position 0: try a-z → explore all words with same letters at positions 1,2,...
* - Position 1: try a-z → explore all words with same letters at positions 0,2,...
* - Position 2: try a-z → explore all words with same letters at positions 0,1,...
*
* This ensures no valid neighbor is missed.
*/
// Loop 1: Try all positions in the word
for (int j = 0; j < arr.length; j++) {
char original = arr[j];
// Loop 2: Try all 26 letters at this position
for (char c : alpha.toCharArray()) {
if (c == original)
continue;
/**
* TRICK: Modify char array in-place to create new word
*
* This is more efficient than string concatenation:
* ✅ String s = beginWord.substring(0,j) + c + beginWord.substring(j+1)
* ← Creates new String each time (slow)
*
* ✅ char[] arr = word.toCharArray();
* arr[j] = c;
* String newStr = new String(arr); ← Reuse array (fast)
*/
arr[j] = c;
String newStr = new String(arr);
if (dict.contains(newStr) && !visited.contains(newStr)) {
/**
* CRITICAL: Mark as visited BEFORE adding to queue
*
* This prevents duplicate enqueuing:
* - If we defer marking until dequeue, multiple neighbors
* could see the same unvisited word and enqueue it multiple times
* - Marking before enqueue ensures each word processed exactly once
*/
visited.add(newStr);
q.add(newStr);
}
}
/**
* CRITICAL: Restore original character AFTER exploring all 26 letters at this position
*
* This is the "backtracking" step:
* - We modified arr[j] to try all 26 letters
* - Before moving to arr[j+1], we must restore arr[j]
* - Otherwise, arr[j+1] modification would operate on wrong base state
*
* Example:
* Position 0: Try 'a','b','c',... → restore to 'h'
* Position 1: Try 'a','b','c',... → restore to 'i' ← must have 'h' at position 0!
* Position 2: Try 'a','b','c',... → restore to 't' ← must have 'h','i' at positions 0,1!
*/
arr[j] = original; // Restore before next iteration
}
}
steps++;
}
return 0; // No path found
}
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: 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 (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 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 |
| 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 |
Problem Categories
1. Tree Traversal Problems
- Level Order Traversal: LC 102, 107, 103
- Binary Tree Paths: LC 257, 1022
- Right Side View: LC 199
- Vertical Order: LC 314
2. Shortest Path Problems
- Unweighted Graphs: LC 127 (Word Ladder)
- Grid Navigation: LC 1730 (Shortest Path to Food), LC 1091 (Shortest Path in Binary Matrix)
- Simultaneous Multi-source Distance (Pattern 4):
- LC 542 (01 Matrix) - Distance to nearest 0 from each cell
- LC 1162 (As Far from Land) - Distance to nearest land from each water cell
- LC 286 (Walls and Gates) - Distance from gates to rooms
- LC 994 (Rotting Oranges) - Time for infection to spread
- Independent BFS Runs (Pattern 4.6):
- LC 317 (Shortest Distance from All Buildings) - Sum of distances to all buildings (use fresh visited for each)
- DFS + Multi-source BFS (Pattern 4.5): LC 934 (Shortest Bridge - mark one component, expand to find other)
- Sequential Targets (Pattern 6): LC 675 (Cut Off Trees for Golf Event - Sort + Repeated BFS)
- Route-Level BFS (Pattern 8): LC 815 (Bus Routes - minimum buses/transfers to reach target)
- State-Based BFS: LC 864 (Shortest Path to Get All Keys), LC 1293 (Shortest Path with Obstacles Elimination)
3. Graph Structure Problems
- Cycle Detection: LC 207 (Course Schedule)
- Connected Components: LC 200 (Number of Islands)
- Graph Validation: LC 261 (Graph Valid Tree)
- Clone Graph: LC 133
4. Matrix/Grid Problems
- Surrounded Regions: LC 130
- Walls and Gates: LC 286
- Maze Problems: LC 490
5. Combination Enumeration Problems (Pattern 9 — BFS-Style Cartesian Product)
- Brace Expansion (LC 1087) — parse into groups, BFS layer-by-layer
- Letter Combinations of a Phone Number (LC 17) — digit → letter group, Cartesian BFS
- Letter Case Permutation (LC 784) — per-char 1-or-2 option groups
- Generalized Abbreviation (LC 320) — keep-or-skip groups per character
Time & Space Complexity
BFS Time Complexity Analysis
BFS time complexity depends on the graph representation:
🔹 Graph Representations
Adjacency List (most common in practice):
- Each vertex is enqueued/dequeued once → O(V)
- Each edge is explored at most once → O(E)
- ✅ Total = O(V + E)
Adjacency Matrix:
- Checking all neighbors of a vertex costs O(V)
- Doing this for all vertices costs O(V²)
- ✅ Total = O(V²)
Detailed Breakdown by Data Structure
Tree BFS
- Time: O(n) - visit each node once
- Space: O(w) - maximum width of tree
- Explanation: Each node visited exactly once, queue stores at most one level
Graph BFS (Adjacency List)
- Time: O(V + E) - visit each vertex and edge once
- Space: O(V) - queue and visited set
- Explanation:
- Vertex processing: Each vertex enqueued/dequeued once = O(V)
- Edge processing: Each edge examined once = O(E)
- Queue space: At most O(V) vertices
- Visited set: O(V) vertices
Graph BFS (Adjacency Matrix)
- Time: O(V²) - check all possible edges
- Space: O(V) - queue and visited set
- Explanation:
- For each vertex, check all V possible neighbors
- Total vertices × neighbors per vertex = V × V = O(V²)
Grid BFS
- Time: O(m × n) - visit each cell once
- Space: O(m × n) - worst case queue size
- Explanation:
- Each cell visited at most once
- Queue can contain at most all cells in worst case
- Grid is essentially a graph with m×n vertices and 4-directional edges
Performance Comparison Table
| Graph Type | Representation | Time Complexity | Space Complexity | Best For |
|---|---|---|---|---|
| Sparse Graph | Adjacency List | O(V + E) | O(V) | E << V² |
| Dense Graph | Adjacency Matrix | O(V²) | O(V²) | E ≈ V² |
| Tree | Parent-Child Links | O(n) | O(w) | Hierarchical data |
| Grid | 2D Array | O(m × n) | O(m × n) | Spatial problems |
Why O(V + E) for Adjacency List?
# Detailed analysis of BFS with adjacency list
def bfs_analysis(graph, start):
queue = deque([start]) # O(1)
visited = {start} # O(1)
while queue: # Executes at most V times
vertex = queue.popleft() # O(1) - each vertex dequeued once
# This inner loop runs exactly deg(vertex) times
for neighbor in graph[vertex]: # Total across all vertices = E
if neighbor not in visited: # O(1) with set
visited.add(neighbor) # O(1) - each vertex added once
queue.append(neighbor) # O(1) - each vertex enqueued once
# Analysis:
# - Outer while loop: O(V) iterations
# - Inner for loop: Sum of deg(v) for all v = 2E (undirected) or E (directed)
# - Each operation inside: O(1)
# Total: O(V + E)
Common Mistakes & Tips
❌ Common Mistakes
- Using
queue.pop()instead ofqueue.popleft()with list - Not handling visited set in graphs (infinite loops)
- Forgetting level-by-level processing when needed
- Incorrect boundary checking in grid problems
✅ Best Practices
- Use
collections.dequefor better performance - Always use visited set for graph problems
- Check boundaries before adding to queue in grid problems
- Consider multi-source BFS for optimization
- Track level/distance when needed for shortest path
- Mark state BEFORE enqueue, not after dequeue — update grid/visited/counters the moment you decide to enqueue a neighbor; deferring until dequeue lets multiple neighbors re-enqueue the same cell (see “When to Update Grid Status & Count” section below)
When to Update Grid Status & Count (Mark Before vs After Enqueue)
A critical BFS implementation detail: always mark a cell as visited (update grid status and counters) BEFORE adding it to the queue, not when you dequeue it.
The Rule
Mark visited + update count → THEN add to queue
General BFS template (canonical form):
visited = {start}
q.append(start)
while q:
node = q.popleft()
for nei in neighbors(node):
if nei not in visited:
visited.add(nei) # <-- before enqueue
q.append(nei)
3-step pattern when state update is non-trivial (grid mutation, counters):
# 1. Validate the neighbor
if neighbor_is_valid and neighbor_not_visited:
# 2. Update state IMMEDIATELY (mark visited / mutate grid / decrement counter)
mark_as_visited(neighbor)
# 3. Enqueue the neighbor
queue.append(neighbor)
// ✅ CORRECT: Mark BEFORE enqueue
if (grid[nr][nc] == 1) {
grid[nr][nc] = 2; // mark immediately
freshOrange--; // update count immediately
q.add(new int[]{nr, nc});
}
// ❌ WRONG: Mark AFTER dequeue
int[] cur = q.poll();
grid[cur[0]][cur[1]] = 2; // too late! duplicates already in queue
Why This Matters
If you defer marking until dequeue, multiple neighbors can enqueue the same cell before any of them processes it:
BFS Layer 1: Cells A and B are both neighbors of cell X (fresh orange)
Thread of execution:
1. Process A → sees X is fresh → enqueues X
2. Process B → sees X is STILL fresh (not marked yet!) → enqueues X AGAIN
3. Dequeue X → mark as rotten, freshOrange--
4. Dequeue X again → already rotten, but freshOrange-- happens again! (WRONG)
Result: Double-counting, incorrect answers, or wasted processing.
Mark-Before-Enqueue guarantees:
| Guarantee | Explanation |
|---|---|
| No duplicates in queue | Cell is marked visited before any other neighbor can see it |
| Correct counting | Each cell counted exactly once |
| O(m x n) time | Each cell enqueued at most once |
| Correct BFS layers | Layer boundaries remain accurate for timing/distance |
Concrete Example: LC 994 - Rotting Oranges
// From RottingOranges.java - V0 solution
while (!q.isEmpty() && freshOrange > 0) {
int size = q.size();
time++;
for (int i = 0; i < size; i++) {
int[] cur = q.poll();
int r = cur[0], c = cur[1];
for (int[] m : moves) {
int nr = r + m[0];
int nc = c + m[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
// CRITICAL: Mark rotten and decrement count BEFORE enqueue
grid[nr][nc] = 2;
freshOrange--;
q.add(new int[] { nr, nc });
}
}
}
}
Python Implementation (LC 994 - Rotting Oranges):
# IDEA: MULTI SRC BFS
# time = O(m × n), space = O(m × n)
from collections import deque
def orangesRotting(grid):
l = len(grid)
w = len(grid[0])
fresh = 0
q = deque()
for y in range(l):
for x in range(w):
if grid[y][x] == 1:
fresh += 1
elif grid[y][x] == 2:
q.append([x, y])
if fresh == 0:
return 0
if not q:
return -1
dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]
time = 0
while q and fresh > 0:
size = len(q)
for _ in range(size):
x, y = q.popleft()
for dx, dy in dirs:
x_ = x + dx
y_ = y + dy
if 0 <= x_ < w and 0 <= y_ < l and grid[y_][x_] == 1:
# NOTE: update RIGHT AWAY — before enqueue
# to avoid the same fresh orange being rotten several times
# (two rotten neighbors in the same layer would both see it as fresh
# and enqueue it twice, causing fresh to go negative)
grid[y_][x_] = 2
fresh -= 1
q.append([x_, y_])
time += 1 # increment AFTER processing the full level (Approach B)
return time if fresh == 0 else -1
If we deferred grid[nr][nc] = 2 (Java) / grid[y_][x_] = 2 (Python) until dequeue, two rotten neighbors processing in the same layer could both enqueue the same fresh orange, leading to fresh going negative and returning a wrong answer.
Cases Where This Applies
| Scenario | Why mark-before-enqueue matters |
|---|---|
| Counting (fresh oranges, infections) | Prevents double-decrement of counters |
| Timing / distance (minutes elapsed) | Ensures cell is assigned to correct BFS layer |
| Grid mutation (spreading rot, flood fill) | Prevents same cell being processed multiple times |
| Visited tracking via grid values | Grid itself serves as visited set; must mark before enqueue |
When Using a Separate visited Set
The same principle applies — add to visited when enqueuing, not when dequeuing:
// CORRECT
if (!visited[nr][nc]) {
visited[nr][nc] = true; // mark BEFORE enqueue
queue.offer(new int[]{nr, nc});
}
// WRONG
int[] cur = queue.poll();
visited[cur[0]][cur[1]] = true; // too late
Related LeetCode Problems
| Problem | Why mark-before-enqueue is critical |
|---|---|
| LC 994 - Rotting Oranges | Counter freshOrange-- must happen exactly once per cell |
| LC 542 - 01 Matrix | Distance assignment must happen on first (shortest) visit |
| LC 286 - Walls and Gates | Room distance must not be overwritten by longer path |
| LC 1162 - As Far from Land as Possible | Same multi-source BFS, distance must be set on first reach |
| LC 200 - Number of Islands | Marking on enqueue prevents re-visiting same land cell |
| LC 934 - Shortest Bridge | Expanding island boundary must not double-count water cells |
| LC 127 - Word Ladder | Words must be marked visited on enqueue to avoid duplicate paths |
Summary
In BFS, the moment you decide a neighbor should enter the queue is the moment you commit — mark it visited, update your counters, mutate the grid. Never defer state changes to dequeue time. This is not an optimization; it is a correctness requirement.
When to Increment Time/Distance: Beginning vs End of BFS Level
A common source of bugs in level-by-level BFS is where to place the time/distance increment. There are two valid approaches, each with different trade-offs.
The Two Approaches
Approach A: Increment at BEGINNING of level (before processing)
// From LC 994 - RottingOranges.java V0
while (!queue.isEmpty() && freshOrange > 0) { // NOTE: extra condition!
int size = queue.size();
time++; // Increment FIRST - we're about to process a "minute" level
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
// process neighbors, infect fresh oranges...
}
}
return freshOrange == 0 ? time : -1;
Approach B: Increment at END of level (only if work was done)
// From LC 994 - RottingOranges.java V0-0-2, V0-1, V0-4
while (!queue.isEmpty()) {
int size = queue.size();
boolean rottedThisMinute = false;
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
// process neighbors...
if (/* infected a fresh neighbor */) {
rottedThisMinute = true;
}
}
if (rottedThisMinute) time++; // Only count if actual infection happened
}
return freshOrange == 0 ? time : -1;
Detailed Comparison
| Aspect | Approach A (Beginning) | Approach B (End with Flag) |
|---|---|---|
| When to increment | Before processing level | After processing, only if work done |
| Extra while condition? | Yes: freshOrange > 0 |
No, flag handles edge cases |
| Risk | Over-counting if condition missing | None if flag used correctly |
| Code complexity | Simpler loop body | Requires tracking boolean flag |
| When returns 0? | Natural if no fresh oranges | Natural: no work = no increment |
Why Approach A Needs freshOrange > 0 in While Condition
The Problem: If we only check !queue.isEmpty(), we’ll increment time for processing already-rotten cells that have nothing left to infect.
Scenario: After all oranges are infected
Layer N: Queue = [(2,1)], freshOrange = 1
- time++ → time = 4
- Process (2,1): infect (2,2)
- freshOrange = 0, Queue = [(2,2)]
Layer N+1: Queue = [(2,2)], freshOrange = 0
- WITHOUT `freshOrange > 0`: time++ → time = 5 (WRONG! over-count)
- WITH `freshOrange > 0`: Exit loop, return time = 4 (CORRECT!)
The Key Insight: When freshOrange == 0, all oranges are ALREADY infected (marked as 2). The queue may still contain rotten cells, but they have no fresh neighbors to infect. Processing them would waste time and over-count.
// CORRECT: Exit early when nothing left to infect
while (!queue.isEmpty() && freshOrange > 0) {
time++;
// ...
}
Why Approach B Naturally Handles Edge Cases
while (!queue.isEmpty()) {
int size = queue.size();
boolean rottedThisMinute = false;
for (int i = 0; i < size; i++) {
// process...
if (/* infected a neighbor */) {
rottedThisMinute = true;
}
}
if (rottedThisMinute) time++; // Only count if actual infection happened
}
Why it works:
- Even if queue has items (previously infected cells)
- If they don’t infect any NEW cells →
rottedThisMinute = false - No increment → no over-counting
Concrete Example: LC 994 - Rotting Oranges
Grid: [[2,1,1], Initial: 6 fresh oranges, 1 rotten at (0,0)
[1,1,0],
[0,1,1]] Expected answer: 4 minutes
Approach A Trace (time++ at beginning with freshOrange > 0):
Initial: Queue=[(0,0)], fresh=6, time=0
Check: queue not empty && fresh>0 → TRUE
time++ → time=1
Process (0,0): infect (0,1), (1,0)
fresh=4, Queue=[(0,1),(1,0)]
Check: queue not empty && fresh>0 → TRUE
time++ → time=2
Process (0,1): infect (0,2), (1,1)
Process (1,0): nothing new
fresh=2, Queue=[(0,2),(1,1)]
Check: queue not empty && fresh>0 → TRUE
time++ → time=3
Process (0,2): nothing (neighbor (1,2)=0)
Process (1,1): infect (2,1)
fresh=1, Queue=[(2,1)]
Check: queue not empty && fresh>0 → TRUE
time++ → time=4
Process (2,1): infect (2,2)
fresh=0, Queue=[(2,2)]
Check: queue not empty && fresh>0 → FALSE (fresh=0)
EXIT LOOP
Return fresh==0 ? time : -1 → time=4 ✓ CORRECT!
What if we removed freshOrange > 0 from while condition?
...continuing from above...
Check: queue not empty → TRUE (Queue=[(2,2)])
time++ → time=5 ← WRONG! Over-counting
Process (2,2): no fresh neighbors
Queue=[]
Return time=5 ✗ WRONG!
Decision Guide: Which Approach to Use?
Use Approach A (time++ at beginning) when:
- ✅ You have a clear “completion” condition (e.g.,
freshOrange == 0) - ✅ You want simpler loop body without tracking flags
- ✅ Problem semantics: “time passes, THEN infection spreads”
- ⚠️ MUST add completion condition to while loop!
Use Approach B (time++ at end with flag) when:
- ✅ No clear completion condition available
- ✅ Want to be safe from over-counting
- ✅ Problem semantics: “infection spreads, THEN time passes”
- ✅ Multiple different “work” types need tracking
Common Patterns in LC 994 Solutions
| Version | Strategy | Key Code |
|---|---|---|
| V0, V0-0-1 | time++ at beginning | while (!q.isEmpty() && freshOrange > 0) { time++; ... } |
| V0-0-2, V0-1, V0-4 | time++ at end with flag | if (rottedThisMinute) time++; |
| V1-1 | time++ at end (no flag) | while (fresh > 0 && !q.isEmpty()) { ... } time++; |
Summary
| Scenario | Recommended Approach |
|---|---|
| Have completion counter (fresh oranges, keys collected) | Approach A with counter in while condition |
| No completion counter | Approach B with boolean flag |
| Want simplest correct code | Approach B (harder to get wrong) |
| Want most efficient code | Approach A (no flag overhead) |
Rule of Thumb: If you use
time++at the BEGINNING, you MUST have an early-exit condition in the while loop. Otherwise, usetime++at the END with a flag.
Advanced Techniques
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
Core Concepts Summary
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:
BFS expands in layers (level-by-level): Layer 0: All sources (distance = 0) Layer 1: All cells 1 step away (distance = 1) Layer 2: All cells 2 steps away (distance = 2) ... When BFS first reaches a cell, it MUST be via the shortest path because all shorter paths were explored in earlier layers.
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)
Quick Reference
When to Use BFS
- Finding shortest path in unweighted graphs
- Level-order tree traversal
- Finding connected components
- Checking if graph is bipartite
- Web crawling (breadth-first exploration)
- Simultaneous multi-source distance calculations (Pattern 4) - distance to nearest source
- Independent BFS runs from multiple sources (Pattern 4.6) - sum of distances to all sources
BFS vs Dijkstra — When to Use Which
| Criteria | BFS | Dijkstra |
|---|---|---|
| Edge weights | All equal (unweighted) or 0/1 | Non-negative, varying weights |
| Data structure | Queue (LinkedList) |
Priority Queue (min-heap) |
| Time complexity | O(V + E) | O((V + E) log V) |
| First visit = shortest? | ✅ Yes (level = distance) | ❌ No (must relax via PQ) |
| “Minimum steps/moves” | ✅ Use BFS | ❌ Overkill |
| “Minimum cost/weight” | ❌ Wrong answer | ✅ Use Dijkstra |
| Grid with uniform cost | ✅ BFS | ❌ Unnecessary overhead |
| Grid with varying costs | ❌ | ✅ Dijkstra on implicit graph |
Decision rule: If every edge has the same cost (or cost is 1), use BFS. The moment edges have different weights, switch to Dijkstra.
Common trap: Using Dijkstra (PQ) for problems like LC 279 Perfect Squares or LC 752 Open the Lock where all edges cost 1 — plain BFS is simpler and faster.
0-1 BFS special case: If edges are weighted 0 or 1 only, use a deque — push weight-0 edges to front, weight-1 edges to back. O(V+E) like BFS, handles two weights correctly.
When NOT to Use BFS
- Deep trees/graphs with limited memory
- Only need to find ANY path (not shortest)
- Weighted graphs with varying costs (use Dijkstra instead)
- Need to explore all paths (use DFS)
Key LeetCode Problems
| Difficulty | Problem | Key Concept | Core Pattern |
|---|---|---|---|
| Easy | LC 102 | Level-order traversal | Pattern 2 (Level-by-Level) |
| Medium | LC 127 | Shortest path transformation - Word Ladder | Pattern 7 (BFS + Backtracking) |
| Medium | LC 200 | Connected components | Pattern 3 (Graph BFS) |
| Medium | LC 742 | Closest leaf (tree → undirected graph) | §2-14 (Tree → Graph + BFS) |
| Medium | LC 542 | Simultaneous multi-source - 01 Matrix | Pattern 4 (Simultaneous Multi-Source) |
| Medium | LC 934 | DFS + Multi-source BFS (island expansion) | Pattern 4.5 (DFS + Multi-Source) |
| Medium | LC 1162 | As Far from Land as Possible | Pattern 4 (Simultaneous Multi-Source) |
| Hard | LC 126 | Find ALL shortest paths - Word Ladder II | Pattern 8 (BFS + DFS DAG Enumeration) |
| Hard | LC 286 | Walls and Gates | Pattern 4 (Simultaneous Multi-Source) |
| Hard | LC 317 | Independent BFS runs (sum of distances) | Pattern 4.6 (Independent BFS Runs) |
| Hard | LC 675 | Sort + Repeated BFS (sequential targets) | Pattern 6 (Sort + Repeated BFS) |
| Hard | LC 752 | BFS + Backtracking on state space - Open the Lock | Pattern 7 (BFS + Backtracking) |
| Hard | LC 815 | Route-level BFS (minimum buses) | Pattern 8 (Route-Level BFS) |
| Hard | LC 864 | BFS with state (key collection) | Pattern 3 + State |
| Hard | LC 1293 | BFS with state (obstacle elimination) | Pattern 3 + State |
LC Examples
2-1) Rotting Oranges (LC 994) — Multi-source BFS
Spread rot from all initial rotten oranges simultaneously level by level.
// LC 994 - Rotting Oranges
// IDEA: Multi-source BFS
// time = O(M*N), space = O(M*N)
public int orangesRotting(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) queue.offer(new int[]{r, c});
else if (grid[r][c] == 1) fresh++;
}
if (fresh == 0) return 0;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int minutes = 0;
while (!queue.isEmpty() && fresh > 0) {
minutes++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
for (int[] d : dirs) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
grid[nr][nc] = 2;
fresh--;
queue.offer(new int[]{nr, nc});
}
}
}
}
return fresh == 0 ? minutes : -1;
}
2-2) Word Ladder (LC 127) — BFS Shortest Transformation
BFS on word graph; each edge connects words differing by one letter.
// LC 127 - Word Ladder
// IDEA: BFS level by level on word transformations
// time = O(M^2 * N), space = O(M^2 * N) M=word length, N=wordList size
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
int steps = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String word = queue.poll();
char[] chars = word.toCharArray();
for (int j = 0; j < chars.length; j++) {
char orig = chars[j];
for (char c = 'a'; c <= 'z'; c++) {
chars[j] = c;
String next = new String(chars);
if (next.equals(endWord)) return steps + 1;
if (wordSet.remove(next)) queue.offer(next);
}
chars[j] = orig;
}
}
steps++;
}
return 0;
}
2-3) Word Ladder II (LC 126) — BFS + DFS All Shortest Paths
BFS builds a DAG of shortest-path predecessors; DFS enumerates all valid paths.
// LC 126 - Word Ladder II
// IDEA: BFS to build predecessors map, then DFS to reconstruct all shortest paths
// time = O(N * M * 26 + output), space = O(N * M)
public List<List<String>> findLadders(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: word → list of predecessors (parents) at shortest distance
Map<String, List<String>> parents = new HashMap<>();
// BFS to build the parent graph
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(beginWord);
visited.add(beginWord);
boolean found = false;
while (!queue.isEmpty() && !found) {
int size = queue.size();
Set<String> levelVisited = new HashSet<>(); // Critical: track nodes at this level
for (int i = 0; i < size; i++) {
String word = queue.poll();
char[] chars = word.toCharArray();
for (int j = 0; j < chars.length; j++) {
char orig = chars[j];
for (char c = 'a'; c <= 'z'; c++) {
if (c == orig) continue;
chars[j] = c;
String next = new String(chars);
if (!wordSet.contains(next)) continue;
if (!visited.contains(next)) {
parents.computeIfAbsent(next, k -> new ArrayList<>()).add(word);
levelVisited.add(next);
if (next.equals(endWord)) found = true;
}
}
chars[j] = orig;
}
}
// Update visited after entire level (allows multiple parents from same level)
visited.addAll(levelVisited);
for (String node : levelVisited) {
queue.offer(node);
}
}
// DFS to enumerate all paths
if (found) {
List<String> path = new LinkedList<>();
dfsEnumerate(endWord, beginWord, parents, path, result);
}
return result;
}
private void dfsEnumerate(String word, String beginWord, Map<String, List<String>> parents,
List<String> path, List<List<String>> result) {
path.add(0, word);
if (word.equals(beginWord)) {
result.add(new ArrayList<>(path));
} else if (parents.containsKey(word)) {
for (String prev : parents.get(word)) {
dfsEnumerate(prev, beginWord, parents, path, result);
}
}
path.remove(0);
}
Key Differences from LC 127:
- LC 127 (Pattern 7): BFS + Backtracking → Find ONE shortest path, early exit
- LC 126 (Pattern 8): BFS + DFS → Find ALL shortest paths, use parent map, DFS enumeration
- Critical Detail:
levelVisitedallows multiple parents from same BFS level (essential for finding all paths)
2-4) Shortest Path in Binary Matrix (LC 1091) — BFS Shortest Path
BFS from top-left to bottom-right through 0-cells (8-directional).
// LC 1091 - Shortest Path in Binary Matrix
// IDEA: BFS — shortest path in unweighted graph
// time = O(N^2), space = O(N^2)
public int shortestPathBinaryMatrix(int[][] grid) {
int n = grid.length;
if (grid[0][0] == 1 || grid[n-1][n-1] == 1) return -1;
int[][] dirs = {{0,1},{1,0},{0,-1},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}};
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{0, 0, 1});
grid[0][0] = 1; // mark visited
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int r = curr[0], c = curr[1], dist = curr[2];
if (r == n-1 && c == n-1) return dist;
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 0) {
grid[nr][nc] = 1;
queue.offer(new int[]{nr, nc, dist + 1});
}
}
}
return -1;
}
2-4) 01 Matrix (LC 542) — Multi-source BFS from All Zeros
Start BFS from all 0-cells simultaneously; distance propagates outward.
// LC 542 - 01 Matrix
// IDEA: Multi-source BFS — enqueue all 0s first, then expand
// time = O(M*N), space = O(M*N)
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n = mat[0].length;
int[][] dist = new int[m][n];
Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) queue.offer(new int[]{i, j});
else dist[i][j] = Integer.MAX_VALUE;
}
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (!queue.isEmpty()) {
int[] cell = queue.poll();
for (int[] d : dirs) {
int nr = cell[0]+d[0], nc = cell[1]+d[1];
if (nr>=0 && nr<m && nc>=0 && nc<n && dist[nr][nc] > dist[cell[0]][cell[1]]+1) {
dist[nr][nc] = dist[cell[0]][cell[1]] + 1;
queue.offer(new int[]{nr, nc});
}
}
}
return dist;
}
2-5) Open the Lock (LC 752) — BFS on State Space
Model each lock combination as a node; BFS finds minimum turns to reach target.
// LC 752 - Open the Lock
// IDEA: BFS on 4-digit combinations; each turn = 1 step
// time = O(10^4 * 4 * 2), space = O(10^4)
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
String start = "0000";
if (dead.contains(start)) return -1;
queue.offer(start);
visited.add(start);
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String curr = queue.poll();
if (curr.equals(target)) return steps;
char[] chars = curr.toCharArray();
for (int j = 0; j < 4; j++) {
char orig = chars[j];
for (int delta : new int[]{1, -1}) {
chars[j] = (char)((orig - '0' + delta + 10) % 10 + '0');
String next = new String(chars);
if (!visited.contains(next) && !dead.contains(next)) {
visited.add(next); queue.offer(next);
}
chars[j] = orig;
}
}
}
steps++;
}
return -1;
}
2-6) Surrounded Regions (LC 130) — BFS from Border
BFS from all border ‘O’ cells; mark reachable ones safe; flip the rest.
// LC 130 - Surrounded Regions
// IDEA: BFS from border O-cells to find non-surrounded regions
// time = O(M*N), space = O(M*N)
public void solve(char[][] board) {
int m = board.length, n = board[0].length;
Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if ((i==0||i==m-1||j==0||j==n-1) && board[i][j]=='O') {
board[i][j] = 'S'; queue.offer(new int[]{i,j});
}
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (!queue.isEmpty()) {
int[] c = queue.poll();
for (int[] d : dirs) {
int nr=c[0]+d[0], nc=c[1]+d[1];
if (nr>=0&&nr<m&&nc>=0&&nc<n&&board[nr][nc]=='O') {
board[nr][nc]='S'; queue.offer(new int[]{nr,nc});
}
}
}
for (int i=0;i<m;i++) for (int j=0;j<n;j++)
board[i][j] = board[i][j]=='S' ? 'O' : (board[i][j]=='O' ? 'X' : board[i][j]);
}
2-7) Course Schedule (LC 207) — BFS Topological Sort (Kahn’s)
Build in-degree array; BFS processes nodes with zero in-degree iteratively.
// LC 207 - Course Schedule
// IDEA: Kahn's BFS topological sort — detect cycle in directed graph
// time = O(V+E), space = O(V+E)
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] inDegree = new int[numCourses];
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>());
for (int[] pre : prerequisites) {
adj.get(pre[1]).add(pre[0]);
inDegree[pre[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) if (inDegree[i] == 0) queue.offer(i);
int processed = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
processed++;
for (int next : adj.get(course))
if (--inDegree[next] == 0) queue.offer(next);
}
return processed == numCourses;
}
2-8) Walls and Gates (LC 286) — Multi-source BFS
Start BFS from all gates (0s) simultaneously; fill rooms with shortest distance.
// LC 286 - Walls and Gates
// IDEA: Multi-source BFS from all gates — propagate distances
// time = O(M*N), space = O(M*N)
public void wallsAndGates(int[][] rooms) {
int m = rooms.length, n = rooms[0].length;
int INF = Integer.MAX_VALUE;
Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if (rooms[i][j] == 0) queue.offer(new int[]{i, j});
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (!queue.isEmpty()) {
int[] cell = queue.poll();
for (int[] d : dirs) {
int nr = cell[0]+d[0], nc = cell[1]+d[1];
if (nr>=0&&nr<m&&nc>=0&&nc<n&&rooms[nr][nc]==INF) {
rooms[nr][nc] = rooms[cell[0]][cell[1]] + 1;
queue.offer(new int[]{nr, nc});
}
}
}
}
2-9) Minimum Height Trees (LC 310) — BFS Leaf Trimming
Repeatedly remove leaf nodes; the remaining 1-2 nodes are the roots of MHTs.
Core Idea — BFS / Layer Trimming (Onion Peeling):
- Think of the tree like an onion. The MHT roots are in the innermost layer
- This is multi-source BFS from leaves inward — NOT BFS from a single root
- Leaves = nodes with degree 1. Remove all leaves simultaneously → their neighbors may become new leaves
- Repeat until ≤ 2 nodes remain. These are the centroids (MHT roots)
- Why ≤ 2? A tree has at most 2 centroids (diameter even → 2, diameter odd → 1)
Example: 0 - 1 - 2 - 3 - 4
Layer 1: remove 0, 4 (leaves)
Layer 2: remove 1, 3 (new leaves)
Result: [2] ✅ (centroid)
Why NOT brute force?
- BFS from every node to compute height → O(N²) → TLE
- Leaf trimming → O(N) — each node and edge processed once
Pattern — When to Recognize This:
| Signal | Meaning |
|---|---|
| Undirected tree + find optimal root | Leaf trimming |
| Minimize max distance to any leaf | Find centroid |
| “Peel layers from outside inward” | Multi-source BFS |
| Degree-based processing on tree | Similar to Kahn’s on DAG |
Two Implementation Styles:
Style 1 — int[] degree array (simpler, preferred):
// LC 310 - Minimum Height Trees
// IDEA: BFS leaf trimming with degree array
// time = O(N), space = O(N)
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
if (n == 1) return Collections.singletonList(0);
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
int[] degree = new int[n];
for (int[] e : edges) {
graph.get(e[0]).add(e[1]);
graph.get(e[1]).add(e[0]);
degree[e[0]]++;
degree[e[1]]++;
}
Queue<Integer> leaves = new LinkedList<>();
for (int i = 0; i < n; i++)
if (degree[i] == 1) leaves.offer(i);
int remaining = n;
while (remaining > 2) {
int size = leaves.size();
remaining -= size;
for (int i = 0; i < size; i++) {
int leaf = leaves.poll();
for (int nei : graph.get(leaf)) {
degree[nei]--;
if (degree[nei] == 1) leaves.offer(nei);
}
}
}
return new ArrayList<>(leaves);
}
Style 2 — Set<Integer> adjacency (O(1) removal, tracks actual edges):
// LC 310 - Using Set for adjacency
// time = O(N), space = O(N)
public List<Integer> findMinHeightTrees_set(int n, int[][] edges) {
if (n == 1) return Collections.singletonList(0);
List<Set<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new HashSet<>());
for (int[] e : edges) { adj.get(e[0]).add(e[1]); adj.get(e[1]).add(e[0]); }
Queue<Integer> leaves = new LinkedList<>();
for (int i = 0; i < n; i++) if (adj.get(i).size() == 1) leaves.offer(i);
int remaining = n;
while (remaining > 2) {
int size = leaves.size();
remaining -= size;
for (int i = 0; i < size; i++) {
int leaf = leaves.poll();
int neighbor = adj.get(leaf).iterator().next();
adj.get(neighbor).remove(leaf);
if (adj.get(neighbor).size() == 1) leaves.offer(neighbor);
}
}
return new ArrayList<>(leaves);
}
Classic Similar LCs:
| LC # | Problem | Connection |
|---|---|---|
| 310 | Minimum Height Trees | Core leaf trimming problem |
| 207 | Course Schedule | Kahn’s algo — same BFS + degree pattern on DAG |
| 210 | Course Schedule II | Kahn’s with ordering output |
| 834 | Sum of Distances in Tree | Tree centroid / rerooting DP |
| 1245 | Tree Diameter | Diameter → centroid is at midpoint |
| 2603 | Collect Coins in a Tree | Leaf trimming to prune unnecessary nodes |
| 863 | All Nodes Distance K in Binary Tree | BFS on tree structure |
| 994 | Rotting Oranges | Multi-source BFS (same layer-by-layer pattern) |
| 542 | 01 Matrix | Multi-source BFS from all zeros |
2-10) Snakes and Ladders (LC 909) — BFS on Board
Model board as graph; BFS finds minimum dice rolls to reach final square.
// LC 909 - Snakes and Ladders
// IDEA: BFS — each square is a node, dice roll = edges
// time = O(N^2), space = O(N^2)
public int snakesAndLadders(int[][] board) {
int n = board.length;
int[] flat = new int[n * n + 1];
int idx = 1; boolean leftToRight = true;
for (int r = n-1; r >= 0; r--) {
if (leftToRight) for (int c = 0; c < n; c++) flat[idx++] = board[r][c];
else for (int c = n-1; c >= 0; c--) flat[idx++] = board[r][c];
leftToRight = !leftToRight;
}
boolean[] visited = new boolean[n*n+1];
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{1, 0});
visited[1] = true;
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int pos = curr[0], steps = curr[1];
for (int dice = 1; dice <= 6 && pos+dice <= n*n; dice++) {
int next = pos + dice;
if (flat[next] != -1) next = flat[next];
if (next == n*n) return steps + 1;
if (!visited[next]) { visited[next] = true; queue.offer(new int[]{next, steps+1}); }
}
}
return -1;
}
2-11) Bus Routes (LC 815) — Route-Level BFS
Map stops to routes; BFS on route IDs counts minimum buses to reach target.
// LC 815 - Bus Routes
// IDEA: BFS on bus route IDs — each layer = one bus ride
// time = O(N*M), space = O(N*M) N=routes, M=avg stops per route
public int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) return 0;
// 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);
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visitedRoutes = new HashSet<>();
Set<Integer> visitedStops = new HashSet<>();
// seed all routes passing through source
for (int r : stopToRoutes.getOrDefault(source, new ArrayList<>())) {
queue.offer(r);
visitedRoutes.add(r);
}
int buses = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int route = queue.poll();
for (int stop : routes[route]) {
if (stop == target) return buses;
if (visitedStops.contains(stop)) continue;
visitedStops.add(stop);
for (int nextRoute : stopToRoutes.getOrDefault(stop, new ArrayList<>())) {
if (!visitedRoutes.contains(nextRoute)) {
visitedRoutes.add(nextRoute);
queue.offer(nextRoute);
}
}
}
}
buses++;
}
return -1;
}
2-12) Pacific Atlantic Water Flow (LC 417) — BFS from Both Oceans
BFS backward from Pacific and Atlantic borders; cells in both sets can flow to both.
// LC 417 - Pacific Atlantic Water Flow
// IDEA: BFS from Pacific border + Atlantic border; intersection = answer
// time = O(M*N), space = O(M*N)
public List<List<Integer>> pacificAtlantic(int[][] heights) {
int m = heights.length, n = heights[0].length;
boolean[][] pac = new boolean[m][n], atl = new boolean[m][n];
Queue<int[]> pq = new LinkedList<>(), aq = new LinkedList<>();
for (int i = 0; i < m; i++) {
pq.offer(new int[]{i,0}); pac[i][0]=true;
aq.offer(new int[]{i,n-1}); atl[i][n-1]=true;
}
for (int j = 0; j < n; j++) {
pq.offer(new int[]{0,j}); pac[0][j]=true;
aq.offer(new int[]{m-1,j}); atl[m-1][j]=true;
}
bfs(heights, pq, pac, m, n);
bfs(heights, aq, atl, m, n);
List<List<Integer>> res = new ArrayList<>();
for (int i=0;i<m;i++) for (int j=0;j<n;j++)
if (pac[i][j]&&atl[i][j]) res.add(Arrays.asList(i,j));
return res;
}
private void bfs(int[][] h, Queue<int[]> q, boolean[][] visited, int m, int n) {
int[][] dirs={{1,0},{-1,0},{0,1},{0,-1}};
while (!q.isEmpty()) {
int[] c=q.poll();
for (int[] d:dirs) {
int nr=c[0]+d[0],nc=c[1]+d[1];
if (nr>=0&&nr<m&&nc>=0&&nc<n&&!visited[nr][nc]&&h[nr][nc]>=h[c[0]][c[1]]) {
visited[nr][nc]=true; q.offer(new int[]{nr,nc});
}
}
}
}
2-13) Perfect Squares (LC 279) — BFS on Abstract Graph (Number Decomposition)
BFS from
ntoward0; each level subtracts a perfect square. First time we reach 0 = minimum count.
// LC 279 - Perfect Squares
// IDEA: BFS — treat each number as a node, edges = subtracting a perfect square
// time = O(N * sqrt(N)), space = O(N)
public int numSquares(int n) {
// Pre-calculate perfect squares up to n
List<Integer> squares = new ArrayList<>();
for (int i = 1; i * i <= n; i++) {
squares.add(i * i);
}
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
queue.offer(n);
visited.add(n);
int level = 0;
while (!queue.isEmpty()) {
level++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int remaining = queue.poll();
for (int square : squares) {
int nextVal = remaining - square;
if (nextVal == 0)
return level; // Found shortest path
if (nextVal < 0)
break; // Squares are sorted, so we can stop
if (!visited.contains(nextVal)) {
visited.add(nextVal);
queue.offer(nextVal);
}
}
}
}
return -1;
}
2-14) Closest Leaf in a Binary Tree (LC 742) — Tree → Graph + BFS ⭐⭐⭐⭐
“Closest” = fewest edges on a binary tree. The catch: from the target you may need to walk upward (to a parent) as well as downward (to children). A plain tree only has child pointers, so first convert the tree into an undirected graph (each node ↔ its parent and children), then run a normal BFS from the target — the first leaf popped is the answer (BFS on an unweighted graph gives shortest #edges).
1) Core Idea
- DFS to build an undirected graph + record the
targetnode + collectleaves.- For each node, add edges both ways:
graph[node]→parentandgraph[parent]→node. - This is the crucial step — it makes the parent reachable, so “going up” becomes a normal edge.
- For each node, add edges both ways:
- BFS from the target node; the first node popped that is a leaf (no children) is closest.
- Equal-weight edges ⇒ BFS guarantees minimal edge count; no need to track distances.
# python — LC 742 (DFS build graph + BFS from target)
from collections import defaultdict, deque
class Solution(object):
def findClosestLeaf(self, root, k):
graph = defaultdict(list) # node -> [neighbors] (undirected)
leaves = set()
target = [None]
def build(node, parent):
if not node:
return
if node.val == k:
target[0] = node
if parent: # connect BOTH directions
graph[node].append(parent)
graph[parent].append(node)
if not node.left and not node.right: # leaf = no children
leaves.add(node)
build(node.left, node)
build(node.right, node)
build(root, None)
# BFS from target; first leaf reached is the closest
q = deque([target[0]])
visited = {target[0]}
while q:
node = q.popleft()
if node in leaves:
return node.val # earliest pop = fewest edges
for nei in graph[node]:
if nei not in visited:
visited.add(nei)
q.append(nei)
2) Why BFS (not DFS)?
| Goal | minimum #edges target → any leaf |
| Edge weights | all equal (1) ⇒ BFS layer = exact distance |
| Why graph, not tree | answer leaf may be above the target → need parent edges |
| Why “first leaf wins” | BFS pops nodes in nondecreasing distance order |
Tree (k=2): As undirected graph, BFS from 2:
1 dist 0: 2
/ \ dist 1: 4, 1
2 3 (leaf) dist 2: 5, 3(leaf) <- returned (3 closer than the 5→6 chain)
/
4
/
5
/
6 (leaf)
3) Similar LC
| LC | Problem | Relation |
|---|---|---|
| 742 | Closest Leaf in a Binary Tree | this — tree→graph, BFS to nearest leaf |
| 863 | All Nodes Distance K in Binary Tree | same tree→graph trick, BFS K levels out |
| 1192 | Critical Connections | tree/graph as undirected, edge traversal |
| 994 | Rotting Oranges | multi-source BFS, “first reach = min dist” idea |
| 542 | 01 Matrix | BFS shortest distance on unweighted grid |
Pattern takeaway: whenever a tree problem needs movement upward (toward parent), convert it to an undirected graph (add parent links via DFS) and switch to graph BFS/DFS. This “tree → graph” reframing is the key to LC 742 and LC 863.
2-15) Populating Next Right Pointers (LC 116 / 117) — Level BFS wires the next links ⭐⭐⭐⭐
Each node has a
nextpointer that should point to the node immediately to its right on the same level (orNULLif it’s the rightmost). This is just a level-order BFS: while processing one level, chain each node to the one dequeued after it. The follow-up (“O(1) extra space”) reuses thenextpointers you just built as a linked list of the level above to wire the level below — no queue needed.
1) Core Idea
next= “the node to my right on the same level.” So process the tree level by level and, inside each level, linkprev.next = curas you pop nodes off the queue.- The last node of every level gets
next = None(queue is emptied per level, so it never points into the next level). - Works for both LC 116 (perfect tree) and LC 117 (any binary tree) — BFS doesn’t care about the tree shape; children are simply enqueued when they exist.
- O(1)-space follow-up: once level L is fully linked, walk it via
nextpointers as if it were a linked list and set thenextpointers of level L+1 — recycling the structure you already built instead of a queue.
2) Pattern
# python — LC 116/117: BFS by layer, chain nodes via prev pointer
from collections import deque
class Solution(object):
def connect(self, root):
# time = O(N), space = O(W) (W = max width / one level)
if not root:
return None
q = deque([root])
while q:
size = len(q)
prev = None
for _ in range(size): # one full level per outer iteration
cur = q.popleft()
if prev: # link previous node -> current
prev.next = cur
prev = cur
if cur.left:
q.append(cur.left)
if cur.right:
q.append(cur.right)
prev.next = None # last node of the level -> NULL
return root
Alternative (peek at queue front instead of tracking prev):
# python — same BFS, use i < size - 1 to point at the next node still in queue
for i in range(size):
cur = q.popleft()
if i < size - 1:
cur.next = q[0] # front of queue = node to the right
if cur.left: q.append(cur.left)
if cur.right: q.append(cur.right)
Follow-up — O(1) space (perfect tree, LC 116): reuse next links, no queue.
# python — walk each level as a linked list to wire the next level
class Solution(object):
def connect(self, root):
# time = O(N), space = O(1)
if not root:
return None
leftmost = root
while leftmost.left: # stop once we reach the leaf level
head = leftmost
while head:
head.left.next = head.right # (1) same parent
if head.next:
head.right.next = head.next.left # (2) across parents
head = head.next # move right via existing links
leftmost = leftmost.left # drop to next level's leftmost
return root
Visual (LC 116):
1 -> NULL
/ \
2 -> 3 -> NULL
/ \ / \
4-> 5->6->7 -> NULL
BFS level 2: prev walks 4→5→6→7, chaining next; last (7) -> NULL.
O(1) trick: from level [2,3], (1) 2.left→2.right = 4→5, (2) 2.right→2.next.left = 5→6, ...
3) Similar LC
| LC | Problem | Relation |
|---|---|---|
| 116 | Populating Next Right Pointers in Each Node | this — perfect tree, BFS or O(1) next-reuse |
| 117 | Populating Next Right Pointers II | same BFS; tree not perfect, so O(1) version needs a dummy head per level |
| 102 | Binary Tree Level Order Traversal | the base level-BFS this is built on |
| 199 | Binary Tree Right Side View | rightmost node per level = last node before next = None |
| 314 | Binary Tree Vertical Order Traversal | level BFS grouping, but keyed by column not by next |
Pattern takeaway: “point to the node on my right” ⇒ level-order BFS, linking nodes in dequeue order and terminating each level with
next = None. For O(1) space, treat the already-linked level as a linked list to build the one below.