Skip to content

Breadth-first search

coreCommon atGoogleMetaAmazonAnthropicRead firstGraph representationQueues and dequesPractise in the Sandbox →

Breadth-first search (BFS) visits nodes in order of distance from the start: all nodes 1 edge away, then all 2 edges away, and so on. A FIFO queue enforces that order. When every edge costs the same, the first time BFS reaches a node is along a shortest path. It runs in O(V + E).

Drop a stone in a pond. The ripple reaches everything 1 metre away before anything 2 metres away. BFS is that ripple on a graph.

The trick is the queue. You take the oldest item from the front and add newly found neighbours to the back. So everything at distance d sits in the queue ahead of everything at distance d + 1. That ordering is the whole proof that BFS finds shortest paths:

  • Invariant: the queue holds at most two distances, d at the front and d + 1 at the back.
  • Consequence: when a node is first discovered from a node at distance d, no shorter route exists, because every node at distance < d was already processed.

Two words you’ll hear: the frontier is the set of discovered-but-unprocessed nodes (the queue). A layer or level is all nodes at the same distance.

Click cells to draw walls, move the source S or the target E, then step through. Numbers are distances. Switch to multi-source to put several S cells in the queue at once and watch one combined ripple.

BFS ripples out one layer at a time
click to edit, then step
S
E
queue (front → back), 1 cells
(0,0) d=0
dequeued0
queue1
dequeued now / final routejust discoveredin the queue (frontier)finished
Put the source in the queue with distance 0 and mark it seen. Marking on enqueue (not on dequeue) keeps each cell in the queue at most once.
1 / 44

Things to try:

  • Open room: the frontier is a diamond, because grid distance with 4-way moves is Manhattan distance.
  • Seal off E with walls: the queue empties and BFS reports it unreachable.
  • Multi-source: each cell ends up labelled with its distance to the nearest source. No extra work: it’s still one BFS.
The problem says… Why BFS fits
“fewest moves / steps / hops / transformations” Unweighted shortest path is exactly what BFS computes.
a grid where each move costs 1 A grid is a graph: cells are nodes, adjacent open cells are edges.
“minimum number of rounds to reach the target” Each round is one BFS layer.
“distance from every cell to the nearest X” Multi-source BFS from all X at once, not one BFS per cell.
“level order”, “nodes at depth k” in a tree A tree is a graph; BFS by layers gives each level.
“is B reachable from A?” BFS or DFS both work; BFS also gives the distance for free.
  1. Put the source in the queue, set its distance to 0, and mark it seen.
  2. Pop the node at the front of the queue.
  3. For each neighbour not yet seen: mark it seen, set dist = dist[u] + 1, record parent = u, push it on the back.
  4. Repeat until the queue is empty (or you just discovered the target). Rebuild a path by following parent from the target back to the source.
bfs + path_to.py
from collections import deque
def bfs(adj: dict[str, list[str]], source: str) -> tuple[dict[str, int], dict[str, str]]:
"""Fewest-edges distance and BFS-tree parent for every node reachable from source."""
dist = {source: 0}
parent: dict[str, str] = {}
queue = deque([source]) # 1. seed the queue with the source
while queue:
u = queue.popleft() # 2. FIFO: nearest unfinished node first
for v in adj.get(u, []):
if v not in dist: # 3. mark on enqueue, so v enters the queue once
dist[v] = dist[u] + 1
parent[v] = u
queue.append(v)
return dist, parent
def path_to(parent: dict[str, str], source: str, target: str) -> list[str]:
"""Walk parent pointers back from target (4. rebuild the route)."""
if target != source and target not in parent:
return []
path = [target]
while path[-1] != source:
path.append(parent[path[-1]])
return path[::-1]
if __name__ == "__main__":
g = {"a": ["b", "c"], "b": ["d"], "c": ["d", "e"], "d": ["f"], "e": ["f"], "f": []}
dist, parent = bfs(g, "a")
assert dist == {"a": 0, "b": 1, "c": 1, "d": 2, "e": 2, "f": 3}
assert path_to(parent, "a", "f") == ["a", "b", "d", "f"]
assert path_to(parent, "a", "z") == []
assert path_to(parent, "a", "a") == ["a"]
print("ok")

When you need “how many rounds”, process the queue one whole layer at a time. Seeding the first layer with several sources gives multi-source BFS: the distance each node gets is to its nearest source, because the rings from all sources grow at the same speed.

layers (multi-source).py
def layers(adj: dict[int, list[int]], sources: list[int]) -> list[list[int]]:
"""Group nodes by distance to the NEAREST source (multi-source BFS, level by level)."""
seen = set(sources)
frontier = list(dict.fromkeys(sources)) # 1. every source starts at distance 0
out: list[list[int]] = []
while frontier:
out.append(frontier) # 2. the whole frontier is one distance layer
nxt: list[int] = []
for u in frontier:
for v in adj.get(u, []):
if v not in seen: # 3. first time reached = nearest source wins
seen.add(v)
nxt.append(v)
frontier = nxt # 4. advance one ring outward
return out
if __name__ == "__main__":
# A path 0-1-2-3-4-5-6 with sources at both ends: they meet in the middle.
path = {i: [j for j in (i - 1, i + 1) if 0 <= j <= 6] for i in range(7)}
assert layers(path, [0, 6]) == [[0, 6], [1, 5], [2, 4], [3]]
assert layers(path, [3]) == [[3], [2, 4], [1, 5], [0, 6]]
assert layers({}, []) == []
print("ok")
Time Extra space
Adjacency list, V nodes, E edges O(V + E): each node is dequeued once, each edge looked at once (twice if undirected) O(V) for dist, parent, queue
Grid with R rows and C columns, 4-way moves O(R·C): V = R·C and E ≤ 4·R·C O(R·C)
Adjacency matrix O(V²): scanning a row costs V even for few neighbours O(V)
Multi-source with k sources Still O(V + E), not k times that O(V)

The queue can hold a whole layer at once. On a wide-open grid that’s about the length of the diamond’s edge, O(R + C). In a “star” graph it’s all V − 1 leaves. That memory peak is the main practical difference from DFS, whose stack is as deep as the longest path.

  • Bidirectional BFS. Search from both ends and stop when the frontiers meet. With branching factor b and distance d, that’s about 2·b^(d/2) nodes instead of b^d. Useful for word-transformation style puzzles with huge implicit graphs.
  • 0-1 BFS. Weights of 0 or 1 only: use a deque, push 0-edges to the front and 1-edges to the back. O(V + E). See shortest paths.
  • BFS over states. When “where am I” isn’t enough (you also carry keys, a remaining budget, a direction), the node is a tuple. See search over states.
  • Implicit graphs. You never build the adjacency list. Generate neighbours on the fly: the 4 grid moves, or every one-letter change of a word.
  • Counting shortest paths. Keep ways[v]; when you reach v from u with dist[v] == dist[u] + 1, add ways[u].
  • list.pop(0) in Python or array.shift() in JS are O(n) each. Use collections.deque, or a head index in JS. .NET’s Queue<T> is fine.
  • Marking too late (see the tip above): duplicates in the queue, sometimes TLE.
  • Forgetting bounds or walls in grid neighbours. Check 0 <= r < R and 0 <= c < C before indexing.
  • Mutating the input grid as your visited set is fast but surprising. Say so if you do it, or use a separate seen.
  • Off-by-one in “minutes” or “rounds”. Is the start round 0 or 1? Does the answer count the last layer that changed anything? Decide out loud.
  • Using BFS on weighted edges. It returns the fewest edges, not the lowest cost.
  • Ask: weighted or unweighted? 4-way or 8-way moves? Can the start equal the target? What if the target is unreachable: -1, None, an exception?
  • Say the invariant: “The queue is sorted by distance, at most two values, so the first time I see a node is its shortest distance.”
  • State complexity as O(V + E), and for a grid, O(R·C).
  • Expected follow-ups: return the actual path (parent pointers), handle many sources, add weights (Dijkstra), add a constraint like “you may break k walls” (state-space BFS), or make the grid huge (bidirectional, or A* with a heuristic).
Check yourself
pick one
You run BFS on a grid but only mark a cell as seen when you pop it from the queue. What goes wrong?
Check yourself
pick one
You need each cell's distance to its nearest exit, and there are k exits in an R×C grid. What's the cheapest correct approach?
Check yourself
pick one
Why does BFS give shortest paths only when all edges have equal weight?

These are classic textbook exercises, not gym questions:

  • Minimum knight moves from one square to another on an 8×8 board.
  • Fill every empty cell with its distance to the nearest gate (walls and gates).
  • Shortest path in a binary matrix with 8-directional moves.
  • Check whether an undirected graph is bipartite by 2-colouring it layer by layer.
  • Print a binary tree level by level, one line per level.