API Kernel:
GridBFSMultiSourceCore Mechanism: Simultaneous wavefront propagation from multiple source cells, computing minimum distances or spread times in a single BFS traversal.
This document presents the canonical multi-source BFS templates for grid-based propagation problems. Each implementation follows consistent naming conventions and includes detailed logic explanations.
- Core Concepts
- Problem Link
- Difficulty
- Tags
- Pattern
- API Kernel
- Problem Summary
- Key Insight
- Template Mapping
- Complexity
- Why This Problem First?
- Common Mistakes
- Related Problems
- Problem Link
- Difficulty
- Tags
- Pattern
- API Kernel
- Problem Summary
- Key Insight
- Delta from Base Template
- Template Mapping
- Complexity
- Why This Problem Second?
- Common Mistakes
- Related Problems
- Problem Link
- Difficulty
- Tags
- Pattern
- API Kernel
- Problem Summary
- Key Insight
- Delta from Base Template
- Template Mapping
- Alternative: In-Place Modification
- Complexity
- Why This Problem Third?
- Common Mistakes
- DP Alternative
- Related Problems
- Problem Comparison
- State Tracking Comparison
- Pattern Evolution
- Code Structure Comparison
- Decision Tree
- Pattern Selection Guide
- Problem Identification Checklist
- Quick Pattern Recognition
- Universal Templates
Traditional single-source BFS starts from one cell and expands outward. Multi-source BFS starts from all source cells simultaneously, processing them as if they were a single "super-source" at distance 0.
This technique is essential when:
- Multiple origins propagate simultaneously (infections, fire spread)
- Computing distances to the nearest source among many
- Level-by-level expansion from a set of starting points
All sources start at the same "time" (distance 0).
Instead of running BFS from each source separately (O(k * m * n) for k sources), we initialize the queue with all sources and run a single BFS (O(m * n)).
Single-Source BFS: Multi-Source BFS:
S S . . . S
| | |
1 1 1 0 1 1 1 0
| | | | | |
2 2 2 2 2 1 1 1 1 1
from collections import deque
from typing import List
def multi_source_bfs(grid: List[List[int]], is_source, is_valid_target) -> int:
"""
Multi-source BFS template for grid problems.
Core invariant:
- All sources start at distance 0 (initialized in queue simultaneously)
- BFS guarantees shortest distance property
- Each cell is visited exactly once
Args:
grid: The 2D grid
is_source: Function to identify source cells
is_valid_target: Function to identify cells that can be reached/modified
Returns:
Maximum distance reached (or -1 if targets remain unreached)
"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
queue = deque()
# Phase 1: Initialize - collect all sources at distance 0
target_count = 0
for r in range(rows):
for c in range(cols):
if is_source(grid[r][c]):
queue.append((r, c))
elif is_valid_target(grid[r][c]):
target_count += 1
# Early termination: no targets to process
if target_count == 0:
return 0
# Phase 2: BFS expansion
DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)]
distance = 0
while queue:
# Process all cells at current distance level
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
# Boundary check and valid target check
if 0 <= nr < rows and 0 <= nc < cols:
if is_valid_target(grid[nr][nc]):
# Mark as visited (convert to source state)
grid[nr][nc] = grid[r][c] # Or mark with distance
queue.append((nr, nc))
target_count -= 1
distance += 1
# Phase 3: Return result based on problem type
return distance - 1 if target_count == 0 else -1| Variant | State Tracking | Use Case | Example |
|---|---|---|---|
| Propagation Timer | Count levels | "How long until all X?" | Rotting Oranges |
| Fill/Replace | Mark with source value | "Fill from boundaries" | Walls and Gates |
| Distance Field | Store min distance | "Distance to nearest X" | 01 Matrix |
# 4-directional movement (standard for most grid BFS)
DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)]
def in_bounds(r: int, c: int, rows: int, cols: int) -> bool:
"""Check if coordinates are within grid boundaries."""
return 0 <= r < rows and 0 <= c < cols
def get_neighbors(r: int, c: int, rows: int, cols: int):
"""Generate valid 4-directional neighbors."""
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
yield nr, nc| Scenario | Single-Source | Multi-Source |
|---|---|---|
| One starting point | ✅ | ❌ Overkill |
| Multiple starting points, need all distances | ✅ (run k times) | ✅ Better |
| Multiple starting points, need min distance | ❌ Inefficient | ✅ Perfect |
| Simultaneous propagation | ❌ Cannot model | ✅ Natural fit |
For an m × n grid with k source cells:
| Approach | Time | Space |
|---|---|---|
| Single-source BFS × k | O(k * m * n) | O(m * n) |
| Multi-source BFS | O(m * n) | O(m * n) |
The multi-source approach visits each cell exactly once regardless of source count.
https://leetcode.com/problems/rotting-oranges/
Medium
- Array
- Breadth-First Search
- Matrix
GridBFSMultiSource - Propagation Timer
GridBFSMultiSource
Given an m × n grid where:
0= empty cell1= fresh orange2= rotten orange
Every minute, fresh oranges adjacent (4-directionally) to rotten oranges become rotten. Return the minimum minutes until no fresh oranges remain, or -1 if impossible.
All rotten oranges spread infection simultaneously each minute. This is the textbook multi-source BFS scenario: initialize the queue with all rotten oranges (sources), then expand level-by-level. Each BFS level represents one minute of propagation.
Initial: After 1 min: After 2 min:
2 1 1 2 2 1 2 2 2
1 1 0 → 2 1 0 → 2 2 0
0 1 1 0 1 1 0 2 1
After 3 min: After 4 min:
2 2 2 2 2 2
→ 2 2 0 → 2 2 0
0 2 2 0 2 2
from collections import deque
from typing import List
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh_count = 0
# Phase 1: Initialize sources and count targets
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2: # Source: rotten orange
queue.append((r, c))
elif grid[r][c] == 1: # Target: fresh orange
fresh_count += 1
# Early exit: no fresh oranges
if fresh_count == 0:
return 0
# Phase 2: Multi-source BFS expansion
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
minutes = 0
while queue:
# Process entire current level
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if grid[nr][nc] == 1: # Fresh orange found
grid[nr][nc] = 2 # Mark as rotten (visited)
queue.append((nr, nc))
fresh_count -= 1
minutes += 1
# Phase 3: Check if all targets reached
# Subtract 1 because we count one extra level after last infection
return minutes - 1 if fresh_count == 0 else -1- Time: O(m × n) - each cell visited at most once
- Space: O(m × n) - queue can hold all cells in worst case
Rotting Oranges is the canonical multi-source BFS problem because:
- Clear physical metaphor: Infection spreading is intuitive
- Explicit simultaneity: "Every minute" makes multi-source nature obvious
- Simple state space: Only 3 cell values (0, 1, 2)
- Direct BFS level = time mapping: Each level is exactly one minute
Once you understand this problem, the pattern transfers directly to other grid propagation scenarios.
- Starting BFS from one rotten orange - Must start from ALL rotten oranges simultaneously
- Forgetting the -1 adjustment - The while loop counts one extra iteration after the last spread
- Not handling edge cases - Empty grid, no fresh oranges, no rotten oranges
- Modifying grid before full level processing - Must process entire level before incrementing time
- LC 286: Walls and Gates (fill with distances)
- LC 542: 01 Matrix (distance to nearest 0)
- LC 1162: As Far from Land as Possible (max distance to land)
https://leetcode.com/problems/walls-and-gates/
Medium
- Array
- Breadth-First Search
- Matrix
GridBFSMultiSource - Fill with Distance
GridBFSMultiSource
Given an m × n grid where:
-1= wall (obstacle)0= gate (destination)INF(2³¹ - 1) = empty room
Fill each empty room with the distance to its nearest gate. If impossible to reach a gate, leave as INF.
Reverse the perspective: Instead of computing distance FROM each room TO gates, compute distance FROM gates TO rooms. Start BFS from all gates simultaneously - each room gets filled with the distance when first reached (which is guaranteed to be the minimum due to BFS properties).
Initial: After BFS:
INF -1 0 INF 3 -1 0 1
INF INF INF -1 → 2 2 1 -1
INF -1 INF -1 1 -1 2 -1
0 -1 INF INF 0 -1 3 4
| Aspect | Rotting Oranges | Walls and Gates |
|---|---|---|
| Source | Rotten (2) | Gate (0) |
| Target | Fresh (1) | Empty (INF) |
| Marking | Change to 2 | Store distance |
| Result | Max time / -1 | Modified grid |
Key change: Instead of just marking visited, we store the distance value in each cell.
from collections import deque
from typing import List
class Solution:
def wallsAndGates(self, rooms: List[List[int]]) -> None:
"""
Modify rooms in-place to store distance to nearest gate.
"""
if not rooms or not rooms[0]:
return
INF = 2147483647
rows, cols = len(rooms), len(rooms[0])
queue = deque()
# Phase 1: Initialize - all gates are sources at distance 0
for r in range(rows):
for c in range(cols):
if rooms[r][c] == 0: # Gate found
queue.append((r, c))
# Phase 2: BFS from all gates simultaneously
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
# Only process empty rooms (INF)
# Rooms already filled have shorter distance
if rooms[nr][nc] == INF:
rooms[nr][nc] = rooms[r][c] + 1
queue.append((nr, nc))- Time: O(m × n) - each cell visited at most once
- Space: O(m × n) - queue can hold all cells in worst case
Walls and Gates introduces distance storage while maintaining the core multi-source BFS structure:
- Different source identification: Gates (0) vs rotten oranges (2)
- Distance propagation:
new_dist = current_dist + 1pattern - Natural visited check:
INFcells are unvisited, non-INF are visited - In-place modification: Common grid modification pattern
- BFS from each empty room - O(m²n²) vs O(mn) with multi-source from gates
- Using separate visited set - The grid itself tracks visited status (INF → distance)
- Processing walls - Must skip
-1cells - Re-processing already filled rooms - Check
rooms[nr][nc] == INFbefore updating
- LC 994: Rotting Oranges (propagation timer)
- LC 542: 01 Matrix (similar distance field)
- LC 1162: As Far from Land as Possible (inverse: distance from land)
https://leetcode.com/problems/01-matrix/
Medium
- Array
- Breadth-First Search
- Matrix
- Dynamic Programming
GridBFSMultiSource - Distance Field
GridBFSMultiSource
Given an m × n binary matrix mat, return the distance of the nearest 0 for each cell. The distance between two adjacent cells is 1.
This is a distance field computation problem. For each cell, we want the shortest distance to ANY zero. Multi-source BFS from all zeros computes this optimally:
- All zeros have distance 0 (they ARE zeros)
- Start BFS from all zeros simultaneously
- First time a cell is reached = its minimum distance to any zero
Input: Output:
0 0 0 0 0 0
0 1 0 → 0 1 0
1 1 1 1 2 1
| Aspect | Rotting Oranges | 01 Matrix |
|---|---|---|
| Source | Rotten (2) | Zero (0) |
| Target | Fresh (1) | One (1) |
| Output | Single value (time) | New matrix (distances) |
| Tracking | Count remaining | Distance matrix |
Key changes:
- Create separate distance matrix (or modify in place)
- Initialize zeros with distance 0
- Initialize ones with infinity (or a sentinel like -1)
from collections import deque
from typing import List
class Solution:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
rows, cols = len(mat), len(mat[0])
queue = deque()
# Phase 1: Initialize distance matrix
# Zeros have distance 0, ones start with infinity
dist = [[0 if mat[r][c] == 0 else float('inf')
for c in range(cols)] for r in range(rows)]
# Add all zeros to queue (sources at distance 0)
for r in range(rows):
for c in range(cols):
if mat[r][c] == 0:
queue.append((r, c))
# Phase 2: BFS from all zeros
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
# Update if we found a shorter path
new_dist = dist[r][c] + 1
if new_dist < dist[nr][nc]:
dist[nr][nc] = new_dist
queue.append((nr, nc))
return distclass SolutionInPlace:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
"""
Modify matrix in-place using -1 as 'unvisited one' marker.
"""
rows, cols = len(mat), len(mat[0])
queue = deque()
# Mark all ones as unvisited (-1), add zeros to queue
for r in range(rows):
for c in range(cols):
if mat[r][c] == 0:
queue.append((r, c))
else:
mat[r][c] = -1 # Unvisited marker
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
distance = 0
while queue:
distance += 1
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if mat[nr][nc] == -1: # Unvisited one
mat[nr][nc] = distance
queue.append((nr, nc))
return mat- Time: O(m × n) - each cell visited at most once
- Space: O(m × n) - queue and distance matrix
01 Matrix consolidates the pattern with a clean output specification:
- Pure distance field: Output is a matrix of distances
- No special states: Just 0s and 1s (cleaner than walls/gates)
- Guaranteed solvability: Every cell is reachable from some zero
- Flexible implementation: Can modify in-place or create new matrix
- BFS from each one - O(m²n²) vs O(mn) with multi-source from zeros
- Wrong initialization - Ones must start as infinity/unvisited, not 1
- Confusing source vs target - Sources are zeros (known distance), targets are ones
- Level counting off-by-one - Zeros are at distance 0, not 1
This problem can also be solved with DP (two passes), but BFS is more intuitive for grid distance problems:
# DP approach (for reference):
# Pass 1: top-left to bottom-right (check top and left)
# Pass 2: bottom-right to top-left (check bottom and right)- LC 994: Rotting Oranges (propagation timer)
- LC 286: Walls and Gates (similar fill pattern)
- LC 1162: As Far from Land as Possible (max min-distance)
- LC 934: Shortest Bridge (BFS between two islands)
| Problem | Source Cells | Target Cells | Output | BFS Termination |
|---|---|---|---|---|
| Rotting Oranges | Rotten (2) | Fresh (1) | Max time or -1 | All fresh infected or queue empty |
| Walls and Gates | Gates (0) | Empty (INF) | Modified grid | Queue empty |
| 01 Matrix | Zeros (0) | Ones (1) | Distance matrix | Queue empty |
| Problem | Visited Marker | Distance Storage | Remaining Count |
|---|---|---|---|
| Rotting Oranges | Change 1→2 | Implicit (levels) | Yes (fresh count) |
| Walls and Gates | Change INF→dist | In grid cell | No |
| 01 Matrix | Change 1→dist or -1→dist | Separate matrix or in-place | No |
┌──────────────────────────────────────────────────────────────────┐
│ Multi-Source BFS Evolution │
└──────────────────────────────────────────────────────────────────┘
Rotting Oranges (Base)
│
│ Core pattern:
│ - Initialize queue with all sources
│ - Process level by level
│ - Track remaining targets
│
▼
┌─────────────────────┐
│ What changes for │
│ Walls and Gates? │
├─────────────────────┤
│ + Store distance │
│ + Different markers │
│ - No count tracking │
│ - No result check │
└─────────────────────┘
│
▼
Walls and Gates
│
│ Distance storage pattern:
│ - dist[nr][nc] = dist[r][c] + 1
│ - INF as unvisited marker
│
▼
┌─────────────────────┐
│ What changes for │
│ 01 Matrix? │
├─────────────────────┤
│ + Clean binary grid │
│ + Return new matrix │
│ - No obstacles │
└─────────────────────┘
│
▼
01 Matrix
# ===== Rotting Oranges =====
# Sources: grid[r][c] == 2
# Targets: grid[r][c] == 1
# Update: grid[nr][nc] = 2
# Result: minutes - 1 if fresh == 0 else -1
# ===== Walls and Gates =====
# Sources: rooms[r][c] == 0
# Targets: rooms[r][c] == INF
# Update: rooms[nr][nc] = rooms[r][c] + 1
# Result: in-place modification (void)
# ===== 01 Matrix =====
# Sources: mat[r][c] == 0
# Targets: mat[r][c] == 1 (or dist[r][c] == inf)
# Update: dist[nr][nc] = dist[r][c] + 1
# Result: distance matrixStart: Grid problem involving distances or propagation?
│
▼
┌───────────────────────┐
│ Multiple starting │
│ points (sources)? │
└───────────────────────┘
│
┌───────┴───────┐
▼ ▼
YES NO
│ │
▼ ▼
┌───────────────┐ Single-source BFS
│ Need minimum │ or other approach
│ distance to │
│ ANY source? │
└───────────────┘
│
▼
Multi-Source BFS
│
┌───────┴───────────┬────────────────┐
▼ ▼ ▼
"Time until all?" "Distance field?" "Fill to nearest?"
│ │ │
▼ ▼ ▼
Rotting Oranges 01 Matrix Walls and Gates
(count levels) (store dist) (store dist)
- ✅ Multiple cells serve as starting points
- ✅ Need shortest distance to the nearest source
- ✅ Simultaneous propagation/spread semantics
- ✅ BFS level = distance relationship is needed
- ✅ Grid is unweighted (all edges have cost 1)
- ✅ Only one starting point
- ✅ Need distance from a specific source to all targets
- ✅ Path finding from A to B
- ✅ Weighted edges (different movement costs)
- ✅ Non-uniform grid (some cells cost more to traverse)
- ✅ Only need to check reachability (no actual distances)
- ✅ Bounded directions (e.g., only down/right movement)
- ✅ Two-pass approach is simpler
When you see a grid problem, ask:
- How many sources? Single vs Multiple
- What's the question? Time/count vs Distance vs Reachability
- Simultaneous or sequential? Multi-source implies simultaneous
- Uniform cost? BFS for uniform, Dijkstra for weighted
| Keyword in Problem | Likely Pattern |
|---|---|
| "minimum time until all" | Multi-source BFS (timer) |
| "distance to nearest" | Multi-source BFS (distance field) |
| "fill from boundary" | Multi-source BFS from edges |
| "spread/propagate/infect" | Multi-source BFS (propagation) |
| "shortest path from A to B" | Single-source BFS |
from collections import deque
from typing import List
def propagation_timer(grid: List[List[int]], SOURCE: int, TARGET: int) -> int:
"""
Count levels until all targets are converted to sources.
Returns: number of levels (time units), or -1 if impossible
Use for: LC 994 (Rotting Oranges)
"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
queue = deque()
target_count = 0
# Initialize: collect sources, count targets
for r in range(rows):
for c in range(cols):
if grid[r][c] == SOURCE:
queue.append((r, c))
elif grid[r][c] == TARGET:
target_count += 1
if target_count == 0:
return 0
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
levels = 0
while queue:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if grid[nr][nc] == TARGET:
grid[nr][nc] = SOURCE
queue.append((nr, nc))
target_count -= 1
levels += 1
return levels - 1 if target_count == 0 else -1Use for: LC 994 (Rotting Oranges), similar "time to spread" problems
from collections import deque
from typing import List
def distance_fill(grid: List[List[int]], SOURCE: int, INF: int) -> None:
"""
Fill each INF cell with distance to nearest SOURCE.
Modifies grid in-place.
Use for: LC 286 (Walls and Gates)
"""
if not grid or not grid[0]:
return
rows, cols = len(grid), len(grid[0])
queue = deque()
# Initialize: all sources at distance 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == SOURCE:
queue.append((r, c))
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if grid[nr][nc] == INF:
grid[nr][nc] = grid[r][c] + 1
queue.append((nr, nc))Use for: LC 286 (Walls and Gates), boundary fill problems
from collections import deque
from typing import List
def distance_matrix(mat: List[List[int]], SOURCE_VAL: int = 0) -> List[List[int]]:
"""
Return new matrix where each cell contains distance to nearest source.
Use for: LC 542 (01 Matrix)
"""
rows, cols = len(mat), len(mat[0])
queue = deque()
# Initialize distance matrix
dist = [[0 if mat[r][c] == SOURCE_VAL else float('inf')
for c in range(cols)] for r in range(rows)]
# Add all sources
for r in range(rows):
for c in range(cols):
if mat[r][c] == SOURCE_VAL:
queue.append((r, c))
DIRECTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
r, c = queue.popleft()
for dr, dc in DIRECTIONS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
new_dist = dist[r][c] + 1
if new_dist < dist[nr][nc]:
dist[nr][nc] = new_dist
queue.append((nr, nc))
return distUse for: LC 542 (01 Matrix), distance field computations
from collections import deque
from typing import List, Callable
def multi_source_bfs_generic(
grid: List[List[int]],
is_source: Callable[[int], bool],
is_target: Callable[[int], bool],
mark_visited: Callable[[List[List[int]], int, int, int], None],
directions: List[tuple] = None
) -> int:
"""
Generic multi-source BFS with customizable predicates.
Args:
is_source: lambda cell_value -> bool
is_target: lambda cell_value -> bool
mark_visited: function(grid, r, c, dist) to mark cell as visited
Returns: max distance reached
"""
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
queue = deque()
directions = directions or [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Initialize
for r in range(rows):
for c in range(cols):
if is_source(grid[r][c]):
queue.append((r, c, 0))
max_dist = 0
while queue:
r, c, dist = queue.popleft()
max_dist = max(max_dist, dist)
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
if is_target(grid[nr][nc]):
mark_visited(grid, nr, nc, dist + 1)
queue.append((nr, nc, dist + 1))
return max_distUse for: Custom multi-source BFS scenarios
Document generated for NeetCode Practice Framework — API Kernel: GridBFSMultiSource