Depth-first search reaches everything but promises nothing about the route it took. Breadth-first search makes the promise: explore in expanding rings, everything one edge away before anything two edges away, and the first time a node is reached, it was reached by a path with the fewest possible edges. The only equipment change is the container. Swap DFS's stack for a queue, and the search stops diving and starts rippling.
That one property makes BFS the answer to every "minimum steps" question on an unweighted graph: fewest moves, fastest spread, shortest transformation. Same O(V + E) cost as DFS, completely different superpowers.
Run it on the same graph the DFS page walks: 0: [1, 2], 1: [0, 3, 4], 2: [0, 4], 3: [1, 5], 4: [1, 2, 5], 5: [3, 4]. From node 0, the rings fall out one at a time: {0} at distance 0, then {1, 2} at 1, then {3, 4} at 2, then {5} at 3. The visit order is 0, 1, 2, 3, 4, 5, where DFS on the identical graph produced 0, 1, 3, 5, 4, 2: same nodes, opposite philosophy.
The shortest-path guarantee needs no cleverness, just the queue's discipline. Every node at distance k entered the queue before any node at k + 1 could, because distance-k + 1 nodes are only ever discovered while processing distance-k ones. Since every edge costs exactly one step, nothing can jump the line, and a node's first discovery is its closest approach.
from collections import deque
def bfs(graph: dict[int, list[int]], start: int) -> dict[int, int]:
# Distance doubles as the visited mark
distance = {start: 0}
queue = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
# Mark on enqueue: each node enters the queue once
if neighbor not in distance:
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return distanceMost interview BFS problems want a number: fewest moves to solve the lock, minutes until every orange rots. The ring structure delivers it through one idiom: snapshot the queue's length at the top of each round, process exactly that many nodes, then add one to the step counter. The snapshot is what separates one ring from the next, because without it, freshly discovered children blend into the current round and the count drifts.
from collections import deque
def min_steps(graph: dict[int, list[int]], start: int, target: int) -> int:
visited = {start}
queue = deque([start])
steps = 0
while queue:
# Snapshot the ring before its children mix in
for _ in range(len(queue)):
node = queue.popleft()
if node == target:
return steps
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
# One full ring processed = one step farther out
steps += 1
return -1Here's the bug that passes every small test and dies on the big one. Marking a node visited when it leaves the queue feels equivalent to marking it when it enters. It isn't. Between entering and leaving, that node is invisible to the visited check, so every other neighbor that discovers it meanwhile adds another copy to the queue. On dense graphs the duplicates compound, and a linear traversal quietly becomes exponential. Mark at enqueue time and each node enters the queue exactly once.
One more belief worth correcting: you might think BFS is the memory-light option since it has no recursion. The truth runs on graph shape. DFS holds one path, O(height), while BFS holds one ring, O(width), and on a bushy graph the widest ring can be half the nodes. Deep and narrow favors BFS. Wide and shallow favors DFS.
The pattern that feels like a cheat code: when the question is "how far is each cell from the nearest X", don't run one search per X. Drop every X into the queue at distance 0 and run a single BFS. The ripples from all sources spread together, and whichever ripple reaches a cell first came from its nearest source, no comparison needed. Rotting Oranges seeds every rotten orange and counts rings as minutes. 01 Matrix seeds every zero. Walls and Gates seeds every gate. One traversal each, O(V + E) total.
The pond picture earns one disclaimer. Ripples spread evenly because every edge costs one step, and the moment edges carry different weights, the nearest ring is no longer the cheapest route. That's where BFS hands the problem to Dijkstra's algorithm, which is the next stop on this path.
0 and mark it immediately. One source for plain searches, every source at once for nearest-of-many problems.k leaves before anything at k + 1 enters the front.len(queue) at the top of each round and process exactly that many nodes. Each completed round is one minute, one move, one transformation, whatever the problem counts.Grid BFS is the bread and butter: Shortest Path in Binary Matrix rings outward through eight directions, and the multi-source family covers nearest-distance grids. The more surprising family treats abstract states as nodes. Word Ladder connects words that differ by one letter and BFSes from the start word, and Open the Lock connects each 4-wheel combination to the eight combinations one click away, with the deadends as walls. Neither problem mentions a graph. Both are pure BFS the moment you see states as places.
On trees, BFS goes by another name, level-order traversal, and the ring snapshot is what groups each depth into its own list. Trees need no visited set (no cycles to guard against), which makes them the gentlest place to practice the queue mechanics.
Five habits that keep ring-by-ring searches honest:
O(n) because everything shifts left, which silently turns the traversal quadratic. collections.deque, ArrayDeque, and their cousins make both ends O(1).Two checks first: why does marking on dequeue flood the queue with duplicate entries, and what property of the edges does first-arrival-is-shortest depend on? Start with level order on trees, then the grid searches, then the state-space puzzles:
Completed | Title | Status | Notes | Solution | |
|---|---|---|---|---|---|
Easy | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Hard |