CLRS — Chapter 20, §20.2: Breadth-First Search — Correctness and Running TimeTextbook
Shortest paths in an unweighted graph
Breadth-first search computes which vertices can be reached from a source and the fewest edges needed to reach each one. Its predecessor information also records a shortest route. This mission belongs to the CLRS fourth-edition formalization series and targets Chapter 20, §20.2, including the algorithm's linear running-time guarantee. The section placement is given in the publisher's table of contents.
Graph, source, and outputs
A directed graph consists of vertices numbered and outgoing adjacency rows. The field adj[u] is an integer sequence, and degree[u] determines its meaningful prefix. Every entry in that prefix must name a valid vertex. Let be the sum of these row lengths: parallel arcs count separately, and self-loops are permitted. An undirected graph is represented by symmetric directed entries, with both directions counted in .
The caller supplies a valid source , with . Consequently, the source contract excludes the empty graph. The distance output uses for an unreachable vertex; otherwise it records a nonnegative number of edges. The parent output uses at the source and unreachable vertices. Other reached vertices have a predecessor. No particular choice between equally short routes is required.
Formalization targets
The headline CLRS.BFS.correctness_and_running_time asks for one actual terminating bfs.Call whose result satisfies correctness, preservation, allocation, and running time together. Write and for its output arrays and for a finite directed walk in the initial graph. For every vertex ,
When , the returned distance is attained and minimal:
Moreover, , , and each reached satisfies
The running-time target is uniform over inputs, heaps, adjacency order, and initial counters:
Here is the increment of graph_work for that same call. The supporting milestone correctness_and_explicit_running_time retains the stronger numerical target
with the identical correctness and storage contract. Both statements are open; the numerical bound is a requested obligation, not an established result.
A reusable shortest-path contract
The result allows a later caller to read shortest distances and predecessor edges while continuing with the returned heap. The caller-facing predicate CLRS.BFS.Post before after result g source includes both the mathematical result and caller-storage preservation. It requires a fresh result object and two distinct fresh arrays, and preserves every previously allocated sequence, object, and collection. The goals combine that postcondition with the same terminating Call and its cost. Input graph storage therefore remains available for further traversals.
The textbook supplies the mathematical algorithm and analysis; the work here is their formal verification against a mutable source program. The finite-prefix specification gives callers useful facts without prescribing irrelevant sequence entries outside the vertex range. The source's discovery state is encoded by distance , and its queue is a fresh sequence with head and tail indices. These are implementation choices within the stated BFS contract. The familiar correctness and complexity claims are discussed in CLRS, fourth edition, §20.2.
What must be justified
Returning a reachable predecessor alone does not establish shortestness. The contract also requires minimum distance and a correct unreachable sentinel. Cycles, repeated adjacency entries, and edges to vertices already discovered must not produce repeated queue insertion or invalidate the cost bound. An execution theorem must establish termination and the result together; reasoning only about executions that happen to return leaves a missing obligation.
Formalization scope
CLRS.GraphTraversal.GraphRep connects the input heap to a finite mathematical graph. Adjacency rows may share references, because traversal only reads them. Validity includes allocated rows and nonnegative degrees. The explicit initialization loop is counted even though the result constructor already gives the same default values. Outputs are fresh, so their mutation cannot change aliased input storage.
The shared prerequisites are the published Algo2Lean runtime, module-call support, and unweighted graph representation and specifications. All three missions reuse these same definitions by reference, without introducing duplicate prerequisite bundles. The existing Dijkstra graph definition uses weighted, object-linked adjacency lists; the shared unweighted module supplies the arrays-of-arrays storage contract used here.
The definitions use Algo2Lean v0.1.4, source commit 0f79d5e4242e6477531c9ee4410409f5b1c92f76, in project mode with standalone=false. The source root contains the explicitly named modules below; the entry file is identified for each mission. Raw generated definitions and platform import-path adaptations are recorded separately in the reproduction manifest. Stable generated definitions supply the public Call relations; optional named-line interfaces are not part of the goal statements.
The clock graph_work counts the marked source blocks, function entries, and evaluated loop guards, including final false guards. Each marked block contains a fixed amount of source work. Costs are differences between the caller's actual initial and final counters; other clocks are preserved. This model uses mathematical integer sequences and unbounded integers. It does not assert machine wall-clock time, integer bit complexity, stack-space bounds, or Lean proof-checking time. Construction of the caller's input graph is outside the traversal entry point.
All theorem bodies are open contributor tasks. Supplied definitions and well-formed goal statements are not correctness, termination, or running-time proofs. The requested results concern the displayed program's actual executions, not an assumed-correct replacement algorithm or a hypothesis containing its conclusion.
The complete source entry is bfs.algo; its shared dependency is graph_input.algo. Contributors may add proof lemmas or named observations while retaining the main source-facing contract.
graph_input.algo
# Unweighted directed graph: vertices are 0, ..., size-1.
# Row adj[u][0:degree[u]] lists outgoing targets, with one slot per arc.
# Traversals require nonnegative degrees and in-range targets. Duplicate arcs
# and self-loops are allowed. The constructor stores the supplied references.
class UnweightedGraph:
size: Int
adj: Seq[Seq]
degree: Seq
def __init__(self, size: Int, adj: Seq[Seq], degree: Seq) -> None:
self.size = size
self.adj = adj
self.degree = degree
bfs.algo
# CLRS, fourth edition, Chapter 20, Section 20.2: breadth-first search.
# Adjacency rows contain vertex identifiers; degree gives each finite row length.
# The graph is read-only. A valid source lies between 0 and graph.size - 1.
from graph_input import UnweightedGraph
class BFSResult:
distance: Seq
parent: Seq
def __init__(self) -> None:
tick("graph_work")
self.distance = const(-1)
self.parent = const(-1)
def bfs(graph: UnweightedGraph, source: Int) -> BFSResult:
tick("graph_work")
result = BFSResult()
vertex: Int = 0
tick("graph_work")
while vertex < graph.size:
tick("graph_work")
result.distance[vertex] = -1
result.parent[vertex] = -1
vertex += 1
tick("graph_work")
tick("graph_work")
queue: Seq = const(-1)
head: Int = 0
tail: Int = 1
queue[0] = source
result.distance[source] = 0
tick("graph_work")
while head < tail:
tick("graph_work")
vertex = queue[head]
head += 1
index: Int = 0
tick("graph_work")
while index < graph.degree[vertex]:
tick("graph_work")
neighbor: Int = graph.adj[vertex][index]
if result.distance[neighbor] == -1:
tick("graph_work")
result.distance[neighbor] = result.distance[vertex] + 1
result.parent[neighbor] = vertex
queue[tail] = neighbor
tail += 1
index += 1
tick("graph_work")
tick("graph_work")
tick("graph_work")
return result
Selected references
- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein, Introduction to Algorithms, fourth edition, MIT Press, 2022. Publisher; fourth-edition table of contents.
- Lunjia Hu and Chutong Yang, Algo2Lean, software release v0.1.4. Lunjia Hu is the project creator. Hosted compiler; fixed release source.