BFS Shortest Paths on Unweighted Graphs
Imagine a social network where you want the fewest introductions to connect two strangers. Or picture a robot navigating a grid where every move costs the same. Both problems ask the same question. What is the minimum number of edges between two nodes? This is the unweighted shortest-path problem. It sits at the foundation of network routing, puzzle solving, and social-graph analysis. Breadth-First Search is the standard tool for this task. But knowing that BFS "explores level by level" is not enough. That is not the same as knowing why it produces exact shortest distances. Many students can trace a BFS run. Yet they cannot prove that the distances it assigns are truly minimal. The gap is filled by a single structural fact about the algorithm's queue. It stays sorted by distance at all times. This property is called queue monotonicity. This article gives a rigorous, self-contained proof that BFS computes minimum edge-count distances. The proof is built entirely on the queue-monotonicity invariant. We also cover the mechanics of predecessor tracking for path reconstruction. We explain the exact complexity accounting. We also cover the traps that cause off-by-one errors in real implementations. The techniques generalize directly to weighted shortest paths. This foreshadows the broader Single-Source Shortest Paths Framework built on relaxation. By the end, you should be able to state the invariant precisely. You should be able to prove it by induction. And you should be able to use it to argue correctness of BFS distance labels. Not just recite that "BFS finds shortest paths."
- BFS computes the minimum number of edges from a source `s` to every reachable vertex, in one linear-time pass.
- The proof rests on queue monotonicity: at every moment the queue's front-to-back order is non-decreasing in distance, and distances differ by at most 1 across the queue.
- Each vertex is enqueued exactly once and assigned `d[v]` the first time it is discovered — this first assignment is provably optimal.
- The algorithm builds a BFS tree via predecessor pointers, letting you reconstruct any shortest path in `O(path length)` time.
- Running time is `O(V + E)` and space is `O(V)`, with every vertex and edge examined a constant number of times.
- The same skeleton — relax an edge, ask "is this a new best distance?" — reappears with weights in Dijkstra's algorithm and the general SSSP framework.
BFS assigns each vertex the length of the first edge-path that reaches it, and the queue's monotonic distance order guarantees that first arrival is always via a shortest path.
The idea
Given an unweighted, directed or undirected graph `G = (V, E)` and a source vertex `s`, the single-source shortest path problem asks for `δ(s, v)`, the minimum number of edges on any path from `s` to `v`, for every vertex `v`. When no path exists, `δ(s, v) = ∞`. Breadth-first search solves this exactly: starting from `s`, it visits all vertices at distance 1, then all vertices at distance 2, and so on, layer by layer. This layered exploration order is what makes BFS a shortest-path algorithm and not merely a graph traversal. The mechanism that enforces this layering is a FIFO queue. Every vertex, when first discovered, is pushed onto the back of the queue and given a distance label equal to (distance of its discoverer) + 1. Because the queue is FIFO, vertices are processed in exactly the order they were discovered — and it is this ordering discipline, not any explicit notion of "distance," that forces BFS to finish an entire layer before starting the next one. We formalize this as the queue-monotonicity invariant: at any moment during execution, if the queue holds vertices `v₁, v₂, …, vₖ` from front to back, then ``` d[v1] ≤ d[v2] ≤ ... ≤ d[vk] ≤ d[v1] + 1 ``` In words: distances in the queue are sorted and span at most two consecutive integers. This one structural fact is the entire engine behind BFS's correctness proof, and it is worth internalizing before looking at any pseudocode. Alongside distance labels, BFS typically also records a predecessor pointer `π[v]`, the vertex from which `v` was first discovered. The predecessor pointers form a BFS tree, a spanning tree (of the reachable component) in which the tree-path from `s` to any `v` is a shortest path in the original graph. This tree is what lets you reconstruct a shortest path, not just know its length.
How it works
BFS maintains three pieces of per-vertex state: a color (`WHITE` = undiscovered, `GRAY` = discovered but not fully processed, `BLACK` = finished), a distance `d[v]`, and a predecessor `π[v]`. The source is discovered first with distance 0, and every other vertex's distance is derived by adding 1 each time it is reached via an edge from a currently-processing vertex. ``` BFS(G, s): for each vertex u in V(G): color[u] = WHITE d[u] = INFINITY π[u] = NIL color[s] = GRAY d[s] = 0 π[s] = NIL Q = empty queue ENQUEUE(Q, s) while Q is not empty: u = DEQUEUE(Q) for each v in Adj(u): if color[v] == WHITE: color[v] = GRAY d[v] = d[u] + 1 π[v] = u ENQUEUE(Q, v) color[u] = BLACK return d, π ``` Reconstructing a shortest path from `s` to a target `t` is a walk backward through predecessors: ``` PRINT-PATH(π, s, t): if t == s: print s else if π[t] == NIL: print "no path exists" else: PRINT-PATH(π, s, π[t]) print t ``` Two design choices deserve emphasis. First, a vertex is marked `GRAY` (and given its final `d` value) the moment it is discovered, not when it is dequeued — this ensures no vertex is enqueued twice, which is what keeps the running time linear. Second, the check `color[v] == WHITE` is the only gate for enqueuing; once a vertex is colored, its distance is frozen forever. The proof below shows why freezing on first discovery is always correct.
Complexity & correctness
Time complexity. Every vertex is enqueued at most once (it turns from `WHITE` to `GRAY` immediately upon enqueuing, and the `WHITE` check prevents re-enqueuing), so the `while` loop body runs `O(V)` times. Inside, the algorithm scans `Adj(u)` for the dequeued `u`; summed over all vertices, this is `O(E)` for directed graphs and `O(2E) = O(E)` for undirected graphs (each edge appears in two adjacency lists). Initialization costs `O(V)`. Total running time: ``` O(V + E) ``` This is optimal in the sense that any correct algorithm must at least read the input, which already costs `Θ(V + E)` in the adjacency-list representation.
Space complexity. The color, distance, and predecessor arrays are each `O(V)`; the queue holds at most `V` vertices at once. Total space is `O(V)`, on top of the `O(V + E)` needed to store the graph itself.
Correctness — the queue-monotonicity invariant. Define `δ(s, v)` as the true minimum number of edges from `s` to `v` (∞ if unreachable). We want to prove that BFS's `d[v]` equals `δ(s, v)` for every vertex, and we do it via the following invariant, checked at the top of every iteration of the `while` loop (i.e., immediately before each `DEQUEUE`): > Invariant Q. If the queue currently contains vertices `v₁, v₂, …, vₖ` in front-to-back order, then > `d[v₁] ≤ d[v₂] ≤ ⋯ ≤ d[vₖ] ≤ d[v₁] + 1`, > and every vertex ever dequeued so far has `d` equal to its final, correct value. Base case. Just after `s` is enqueued, the queue contains only `s` with `d[s] = 0`. The chain of inequalities holds trivially (a single element). Inductive step. Suppose Invariant Q holds when the queue is `v₁, …, vₖ` (front to back). We dequeue `u = v₁`. By the invariant, `d[u] = d[v₁]` is the smallest distance value present, and every other queued vertex has distance `d[u]` or `d[u] + 1`. Process `Adj(u)`: for each white neighbor `w`, we set `d[w] = d[u] + 1` and append `w` to the back of the queue. Two things must be checked to re-establish Invariant Q for the new queue `v₂, …, vₖ, w₁, …, wₘ`: 1. The remaining old entries `v₂, …, vₖ` still satisfy `d[v₂] ≤ ⋯ ≤ d[vₖ] ≤ d[u] + 1` (this is inherited unchanged from before, since we only removed the front element). 2. Every newly appended `wⱼ` has `d[wⱼ] = d[u] + 1`, which is `≥ d[vₖ]` (since `d[vₖ] ≤ d[u] + 1` from point 1) — so appending them at the back preserves the non-decreasing order — and it is `≤ d[v₂] + 1` since `d[v₂] ≥ d[u]` implies `d[v₂] + 1 ≥ d[u] + 1 = d[wⱼ]`. Hence the new queue again satisfies the "sorted, span-at-most-one" property. This completes the induction, so Invariant Q holds throughout the entire execution.
Deriving `d[v] = δ(s, v)` from Invariant Q. We now prove, by strong induction on `δ(s, v)`, that whenever BFS assigns a finite `d[v]`, it equals `δ(s, v)`, and that BFS assigns a finite `d[v]` to every vertex reachable from `s`.
- `δ(s, s) = 0`, and BFS sets `d[s] = 0` directly. Correct.
- Suppose every vertex `v` with `δ(s, v) = k` is correctly discovered with `d[v] = k` (inductive hypothesis for all values `< k+1`). Let `w` be any vertex with `δ(s, w) = k + 1`. By definition of shortest-path distance, `w` has some neighbor `u` on a shortest path with `δ(s, u) = k`. By the inductive hypothesis `u` is discovered with `d[u] = k`, and since `u` is eventually dequeued (every discovered vertex is eventually dequeued, because the queue only shrinks by removing what it grows by, and the algorithm halts only when empty), BFS scans `Adj(u)` and reaches `w`. If `w` is still `WHITE` at that moment, it is assigned `d[w] = d[u] + 1 = k + 1`, matching `δ(s, w)`.
- We must rule out `w` being colored before this scan with a wrong (too-small or too-large) distance. Suppose for contradiction `w` were discovered earlier via some other vertex `x` with `d[w] = d[x] + 1 ≠ k+1`. Since `δ(s,w) = k+1` is the true minimum, no path of length `< k+1` reaches `w`, so `d[x] + 1` cannot be `< k+1`; thus if it is wrong, it must be `d[x] + 1 > k + 1`, i.e., `d[x] ≥ k + 1`. But Invariant Q says all vertices currently in the queue (which includes `x`, since `x` must be dequeued at or before the time `u` is dequeued for `w` to have already been discovered through `x`) have distances within 1 of the front, and `u` — with `d[u] = k` — is in or has passed through the front of the queue no later than `x`. This forces `d[x] ≤ d[u] + 1 = k+1`, and combined with `d[x] ≥ k+1` gives `d[x] = k+1`— but then `x` itself would need `δ(s,x) ≤ k` to have been dequeued at a point where the queue front distance is `≤ k`, which by the inductive hypothesis on smaller distances means `x` was assigned correctly, contradicting `δ(s,x) \ne k`. Careful bookkeeping of Invariant Q's "span at most 1" clause is precisely what blocks any vertex from ever being discovered too early — the queue simply cannot contain a vertex with distance `k` while simultaneously exposing a would-be distance-`(k+2)` discovery, since all frontier distances present differ by at most 1.
The clean takeaway, stated without the contradiction bookkeeping: because the queue never holds distances spanning more than two consecutive integers, vertices are discovered in non-decreasing order of true distance, so the first time any vertex is colored is necessarily via a shortest path. Since BFS freezes `d[v]` at first discovery and never revisits a colored vertex, `d[v] = δ(s, v)` for all reachable `v`, and unreachable vertices retain `d[v] = ∞` since they are never enqueued. This is the full correctness argument.
Correctness of the BFS tree. Because `π[w] = u` is only set at the moment `d[w] = d[u] + 1` is assigned, following `π` pointers from any `v` back to `s` traces a path whose length is exactly `d[v] = δ(s, v)` — hence a genuine shortest path, not merely a path of that length by coincidence.
Tradeoffs. BFS is optimal for unweighted graphs but gives no useful information when edges have weights (it would count edges, not weighted cost). It also requires `Θ(V)` memory even for sparse queries, and it computes distances to all reachable vertices rather than to a single target, which is wasted work if only one target is needed (though early termination when the target is dequeued is a common, safe optimization since distances are finalized on discovery, not dequeue).
Worked example
Consider the following small directed graph, with `s = A`: ``` A → B, A → C B → D C → D, C → E D → F E → F ``` Trace of the queue and distance assignments:
| Step | Dequeue `u` | Queue after (front→back) | Newly discovered (with `d`) |
|---|---|---|---|
| init | — | `[A]` | `A:0` |
| 1 | `A` | `[B, C]` | `B:1`, `C:1` |
| 2 | `B` | `[C, D]` | `D:2` |
| 3 | `C` | `[D, E]` | `E:2` (D already colored, skipped) |
| 4 | `D` | `[E, F]` | `F:3` |
| 5 | `E` | `[F]` | — (F already colored, skipped) |
| 6 | `F` | `[]` | — |
Final distances: `d[A]=0, d[B]=1, d[C]=1, d[D]=2, d[E]=2, d[F]=3`. Check against the invariant: right after step 1, the queue is `[B, C]` with `d = [1, 1]` — non-decreasing, span 0. After step 2, `[C, D]` with `d = [1, 2]` — non-decreasing, span exactly 1, never more. After step 3, `[D, E]` with `d = [2, 2]`. This matches Invariant Q at every step, and no vertex's `d` value is ever revised after being set — exactly as the proof predicts. Predecessors: `π[B]=A, π[C]=A, π[D]=B` (since `B` was processed before `C`, `D` is first discovered from `B`, not `C`), `π[E]=C`, `π[F]=D` (since `D` is dequeued at step 4, before `E` is dequeued at step 5, so `F` is discovered from `D` first). Reconstructing the shortest path to `F`: `π[F]=D, π[D]=B, π[B]=A`, giving `A → B → D → F`, length 3 — matching `d[F] = 3` exactly, and note this is shorter than the alternative route `A → C → E → F`, which is also length 3 in this particular graph, illustrating that BFS finds a shortest path, not necessarily a unique one, but always of minimum length.
Common pitfalls & traps
| Pitfall | Why it breaks correctness/efficiency |
|---|---|
| Marking a vertex visited at dequeue time instead of at discovery (enqueue) time | Allows the same vertex to be enqueued multiple times, inflating running time to possibly `O(VE)` and corrupting distance values, since a stale `d` from a longer path may overwrite the correct shorter one |
| Forgetting to check `color[v] == WHITE` before enqueuing | Re-adds already-processed vertices, breaking the "each vertex enqueued once" argument the `O(V+E)` bound depends on |
| Using a stack (DFS-style) instead of a FIFO queue | Destroys queue monotonicity entirely; distances computed this way are not guaranteed to be shortest-path distances |
| Confusing "BFS gives shortest paths" with weighted graphs | BFS counts edges, not weighted cost; on weighted graphs it requires Dijkstra's algorithm or Bellman–Ford instead |
| Off-by-one in distance initialization (starting `d[s]` at 1 instead of 0) | Shifts every distance label by one, breaking downstream code that assumes `d[s] = 0` |
| Not handling disconnected components | Vertices unreachable from `s` must retain `d = ∞`; treating them as `0` or leaving stale garbage values causes silent bugs |
Pioneers & history
Breadth-first search traces back to work on maze-solving and network routing in the mid-20th century, later formalized as a general graph-traversal paradigm and analyzed rigorously for shortest-path correctness in the algorithms literature of the 1970s–80s.
Origins · 1 minEdward F. Moore and how this was discovered
Moore described an algorithm for finding shortest paths through mazes in 1959, widely credited as the first clear statement of breadth-first search as a shortest-path method, predating its formal adoption into computer science curricula and its later use in circuit routing (the "Lee algorithm," independently discovered by C. Y. Lee for wire routing on chips).
Where this matters
Unweighted shortest paths via BFS underlie systems where every "hop" has equal cost. Social networks use BFS to compute degrees of separation and mutual-connection suggestions. Web crawlers use BFS-order traversal to explore pages level by level, which naturally prioritizes pages closer (in link-distance) to seed URLs. Peer-to-peer and overlay networks use BFS-like flooding to determine hop-count distances between nodes. Puzzle solvers — the 15-puzzle, Rubik's Cube "God's number" searches on small state spaces, maze games — use BFS because each legal move counts as one unit-cost edge in the state graph. Network topology tools compute shortest routing paths in hop count using BFS before layering on weighted metrics. It is also the base case that motivates the broader Single-Source Shortest Paths Framework: replacing the FIFO queue with a priority queue ordered by tentative distance yields Dijkstra's algorithm, and BFS's discovery-order argument becomes the seed for the greedy-relaxation correctness proofs used there. Understanding BFS's invariant deeply is what makes the leap to weighted shortest paths, and eventually to Dijkstra's Algorithm and Bellman-Ford Algorithm, much more natural.
:::quiz[If BFS discovers vertex w via vertex u with d[u] = k, what distance is assigned to w?]
- d[w] = k, same as u
- d[w] = k - 1
- d[w] = k + 1 ✓
- d[w] is left as infinity until dequeued
Ask the tutor: How does the queue-monotonicity argument change when we move from BFS to Dijkstra's algorithm with a priority queue instead of a FIFO queue?
Does BFS work on directed graphs, or only undirected ones?
BFS works correctly on directed graphs exactly as described: it explores `Adj(u)` which in a directed graph means only outgoing edges from `u`. The distances computed are then directed distances — `δ(s, v)` measured along directed paths — which may differ from `δ(v, s)`. The queue-monotonicity proof makes no use of edge symmetry; it only relies on the FIFO discovery order, so it applies unchanged to directed graphs. The only practical difference is that in directed graphs some vertices may be unreachable from `s` even if `s` is reachable from them, so `d[v] = ∞` occurs more often than in undirected graphs.
Can BFS handle graphs with cycles?
Yes, and the coloring scheme is precisely what makes cycles safe. When BFS follows a cycle back toward an already-discovered vertex, that vertex is `GRAY` or `BLACK`, so the `WHITE` check skips it — it is never re-enqueued or re-labeled. This is essential: without the color check, cycles could cause infinite loops or repeated processing. The invariant proof does not assume acyclicity anywhere; it only uses the enqueue-order and distance-increment rules, so cyclic graphs are handled correctly and efficiently.
What happens if the graph is disconnected?
BFS as described only discovers vertices reachable from `s`; any vertex `v` with no path from `s` is never enqueued, so it retains `d[v] = ∞` and `π[v] = NIL` from initialization. This is correct, since `δ(s, v) = ∞` by definition when no path exists. To compute shortest paths from every vertex to every reachable vertex in all components, you would run BFS separately from each unvisited vertex, which is exactly the "BFS forest" construction used in connected-components algorithms.
How is the BFS tree different from the original graph, and why is it useful?
The BFS tree consists only of the `π` predecessor edges — one edge into each non-source vertex, chosen at the moment that vertex was first discovered. It is a subgraph of `G` (specifically a spanning tree of the component reachable from `s`), and crucially, the tree-path from `s` to any vertex `v` has length exactly `δ(s, v)`. This means you can extract a shortest path in `O(length of path)` time by walking `π` pointers backward, without ever re-searching the graph. The tree also reveals structural information, such as bipartiteness (checked via any BFS tree edge connecting two vertices at the same level) and the graph's diameter (via BFS from multiple sources).
Is BFS the same as level-order traversal in trees?
Essentially yes — level-order traversal of a tree is BFS run on that tree with the root as source, and the "levels" correspond exactly to BFS distance labels. The generalization from trees to arbitrary graphs adds the need for the color array, since general graphs can have cycles and multiple paths to the same vertex, which trees never do. The queue mechanics — enqueue children, dequeue front, repeat — are identical in both settings.
Why can't we just use DFS instead of BFS for shortest paths?
Depth-First Search explores as far as possible along one branch before backtracking, so the order in which it discovers vertices bears no fixed relationship to their true graph distance from the source. A DFS distance label — if you tried to define one analogously — would reflect the length of whichever path DFS happened to explore first, not the shortest one. The queue-monotonicity invariant that makes BFS correct fundamentally depends on FIFO order; a stack (LIFO), which is what DFS effectively uses via recursion, does not preserve non-decreasing distance order at all.
Does the choice of adjacency-list order affect which shortest path BFS finds, and does it matter?
Yes — if multiple shortest paths of the same minimum length exist, the specific path recorded in `π` depends on the order neighbors are listed in `Adj(u)`, since whichever neighbor is scanned first "claims" the predecessor pointer for any shared descendant. However, this never affects the distance `d[v]`, which is uniquely determined by the invariant proof regardless of adjacency order. So BFS is deterministic in the lengths it reports but not necessarily in which exact shortest path it reconstructs.