256 lines
7.5 KiB
YAML
256 lines
7.5 KiB
YAML
name: BFS (Breadth-First Search)
|
|
slug: bfs
|
|
difficulty_level: 3
|
|
|
|
description: >
|
|
Level-by-level traversal using a queue, exploring all neighbors at the current
|
|
depth before moving deeper. This guarantees the shortest path in unweighted
|
|
graphs and is ideal for problems requiring layer-by-layer processing.
|
|
|
|
when_to_use: |
|
|
- Shortest path in unweighted graphs
|
|
- Level-order tree traversal
|
|
- Finding all nodes at distance K
|
|
- Word ladder / transformation problems
|
|
- Spreading simulations (rotting oranges, infection)
|
|
|
|
metaphor: |
|
|
Imagine dropping a stone into a still pond. Ripples spread outward in concentric
|
|
circles—first touching everything 1 meter away, then 2 meters, then 3 meters.
|
|
BFS works the same way: it explores all nodes at distance 1 before any node at
|
|
distance 2.
|
|
|
|
Another analogy: searching for someone in a building. You check everyone on your
|
|
current floor before taking the stairs to the next floor. You never go upstairs
|
|
until you've searched every room on the current level.
|
|
|
|
core_concept: |
|
|
BFS uses a **queue** (FIFO) to process nodes in the order they were discovered.
|
|
This guarantees that when you first reach a node, you've taken the shortest path
|
|
to get there (in terms of number of edges).
|
|
|
|
The key insight is that the queue naturally separates nodes by their distance
|
|
from the source. Everything added in round 1 is at distance 1. Everything added
|
|
in round 2 is at distance 2. By the time you dequeue a node, all closer nodes
|
|
have already been processed.
|
|
|
|
**Why it finds shortest paths**: If there were a shorter path to node X, you
|
|
would have discovered X earlier (through that shorter path) and already
|
|
processed it. The first time you reach X is always via the shortest route.
|
|
|
|
visualization: |
|
|
**Example: Shortest path from A to F**
|
|
|
|
```
|
|
Graph:
|
|
A --- B --- E
|
|
| |
|
|
C --- D --- F
|
|
|
|
BFS from A:
|
|
|
|
Queue: [A] Visited: {A} Level 0
|
|
Process A → add B, C
|
|
|
|
Queue: [B, C] Visited: {A,B,C} Level 1
|
|
Process B → add E, D
|
|
Process C → add D (skip, visited)
|
|
|
|
Queue: [E, D] Visited: {A,B,C,E,D} Level 2
|
|
Process E → no new neighbors
|
|
Process D → add F
|
|
|
|
Queue: [F] Visited: {A,B,C,E,D,F} Level 3
|
|
Process F → Found! Distance = 3
|
|
```
|
|
|
|
**Level-order tree traversal:**
|
|
|
|
```
|
|
1
|
|
/ \
|
|
2 3
|
|
/ \ \
|
|
4 5 6
|
|
|
|
Level 0: [1]
|
|
Level 1: [2, 3]
|
|
Level 2: [4, 5, 6]
|
|
|
|
Result: [[1], [2,3], [4,5,6]]
|
|
```
|
|
|
|
code_template: |
|
|
from collections import deque
|
|
|
|
def bfs_shortest_path(graph: dict, start: str, end: str) -> int:
|
|
"""Find shortest path distance in unweighted graph."""
|
|
if start == end:
|
|
return 0
|
|
|
|
queue = deque([start])
|
|
visited = {start}
|
|
distance = 0
|
|
|
|
while queue:
|
|
distance += 1
|
|
# Process all nodes at current level
|
|
for _ in range(len(queue)):
|
|
node = queue.popleft()
|
|
|
|
for neighbor in graph[node]:
|
|
if neighbor == end:
|
|
return distance
|
|
if neighbor not in visited:
|
|
visited.add(neighbor)
|
|
queue.append(neighbor)
|
|
|
|
return -1 # No path found
|
|
|
|
|
|
def bfs_level_order(root) -> list[list]:
|
|
"""Level-order traversal of binary tree."""
|
|
if not root:
|
|
return []
|
|
|
|
result = []
|
|
queue = deque([root])
|
|
|
|
while queue:
|
|
level = []
|
|
for _ in range(len(queue)):
|
|
node = queue.popleft()
|
|
level.append(node.val)
|
|
|
|
if node.left:
|
|
queue.append(node.left)
|
|
if node.right:
|
|
queue.append(node.right)
|
|
|
|
result.append(level)
|
|
|
|
return result
|
|
|
|
|
|
def bfs_matrix(grid: list[list[int]], start: tuple) -> int:
|
|
"""BFS on a 2D grid."""
|
|
rows, cols = len(grid), len(grid[0])
|
|
queue = deque([start])
|
|
visited = {start}
|
|
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
|
distance = 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
|
|
and (nr, nc) not in visited
|
|
and grid[nr][nc] != 0): # 0 = wall
|
|
visited.add((nr, nc))
|
|
queue.append((nr, nc))
|
|
|
|
distance += 1
|
|
|
|
return distance
|
|
|
|
recognition_signals:
|
|
- "shortest path"
|
|
- "minimum steps"
|
|
- "level order"
|
|
- "nearest"
|
|
- "unweighted graph"
|
|
- "distance"
|
|
- "spreading"
|
|
- "layer by layer"
|
|
- "all nodes at distance K"
|
|
- "word ladder"
|
|
- "rotting oranges"
|
|
|
|
common_mistakes:
|
|
- title: Adding to visited after dequeuing instead of when enqueuing
|
|
description: |
|
|
Marking nodes as visited when you dequeue them (instead of when you
|
|
enqueue them) causes the same node to be added multiple times, leading
|
|
to incorrect distances and wasted processing.
|
|
fix: |
|
|
Always mark nodes as visited immediately when adding to the queue:
|
|
```python
|
|
if neighbor not in visited:
|
|
visited.add(neighbor) # Mark NOW
|
|
queue.append(neighbor)
|
|
```
|
|
|
|
- title: Not tracking levels correctly
|
|
description: |
|
|
Processing the entire queue without tracking level boundaries makes it
|
|
impossible to know the distance or handle level-by-level logic.
|
|
fix: |
|
|
Use the "process current level size" pattern:
|
|
```python
|
|
for _ in range(len(queue)): # Fixed size at level start
|
|
node = queue.popleft()
|
|
# Process node
|
|
distance += 1 # Increment after level completes
|
|
```
|
|
|
|
- title: Using a list instead of deque
|
|
description: |
|
|
Using `list.pop(0)` is O(n) because all elements must shift. This turns
|
|
O(V+E) BFS into O(V²).
|
|
fix: |
|
|
Use `collections.deque` which has O(1) popleft:
|
|
```python
|
|
from collections import deque
|
|
queue = deque([start])
|
|
```
|
|
|
|
- title: Forgetting to check bounds in grid BFS
|
|
description: |
|
|
Moving to invalid coordinates (negative or beyond grid dimensions) causes
|
|
index errors.
|
|
fix: |
|
|
Always validate coordinates before accessing the grid:
|
|
```python
|
|
if 0 <= nr < rows and 0 <= nc < cols:
|
|
# Safe to access grid[nr][nc]
|
|
```
|
|
|
|
variations:
|
|
- name: Shortest path (unweighted)
|
|
description: |
|
|
Classic BFS to find minimum number of edges between two nodes.
|
|
example: "Word Ladder, Minimum Genetic Mutation, Open the Lock"
|
|
|
|
- name: Level-order traversal
|
|
description: |
|
|
Process tree nodes level by level, returning grouped results.
|
|
example: "Binary Tree Level Order Traversal, Zigzag Level Order"
|
|
|
|
- name: Multi-source BFS
|
|
description: |
|
|
Start BFS from multiple sources simultaneously. Useful for "spreading"
|
|
problems where multiple starting points expand in parallel.
|
|
example: "Rotting Oranges, Walls and Gates, 01 Matrix"
|
|
|
|
- name: Bidirectional BFS
|
|
description: |
|
|
Search from both start and end simultaneously, meeting in the middle.
|
|
Reduces search space from O(b^d) to O(b^(d/2)).
|
|
example: "Word Ladder (optimized), Shortest Path in large graphs"
|
|
|
|
- name: BFS with state
|
|
description: |
|
|
Track additional state beyond position (e.g., keys collected, walls broken).
|
|
State becomes part of the "node" being visited.
|
|
example: "Shortest Path in Grid with Obstacles Elimination"
|
|
|
|
related_patterns:
|
|
- dfs
|
|
- tree-traversal
|
|
- matrix-traversal
|
|
|
|
prerequisite_patterns: []
|