Part VI · Graph Algorithms Chapter 20

Elementary Graph Algorithms

Two ways to store a graph and two ways to walk it — and almost every graph algorithm in the rest of the book is one of those walks with bookkeeping attached.

Part VI opens with the foundations: how to represent a graph, and the two systematic traversals. Breadth-first search explores by distance and yields shortest paths in unweighted graphs. Depth-first search explores by following edges as far as possible, and its timestamp structure yields topological sort and strongly connected components almost for free. If you know BFS and DFS cold, the rest of Part VI is variations.

4th edition note. Renumbered from 22 to 20. Content is unchanged: representations, BFS, DFS, topological sort, strongly connected components.

Contents

  1. Representations
  2. Breadth-first search
  3. What BFS computes
  4. Depth-first search
  5. Timestamps and the parenthesis theorem
  6. Edge classification
  7. Topological sort
  8. Strongly connected components
  9. Recap

Representations

A graph G = (V, E) has two standard representations, and the choice matters.

Adjacency listAdjacency matrix
StructureArray of |V| lists; Adj[u] holds the neighbours of u|V| × |V| matrix with aᵢⱼ = 1 if (i,j) ∈ E
SpaceΘ(V + E)Θ(V²)
Test if (u,v) ∈ EO(degree(u))Θ(1)
Enumerate neighbours of uΘ(degree(u))Θ(V)
Preferred forSparse graphs, |E| ≪ |V|² — the usual caseDense graphs, or when you need O(1) edge tests
CLRS defaults to adjacency lists, because most real graphs are sparse. That choice is why so many bounds in Part VI read O(V + E) rather than O(V²). For weighted graphs, store the weight alongside each neighbour in the list, or in the matrix entry.

A note on notation used throughout: inside asymptotic bounds CLRS writes V and E for |V| and |E|. So O(V + E) means O(|V| + |E|).

Breadth-first search

Given a source s, BFS explores the graph in order of increasing distance, discovering all vertices at distance k before any at distance k+1.

Each vertex has a colour recording its state: white (undiscovered), grey (discovered, neighbours not yet all explored), black (finished).

BFS(G, s) 1 for each vertex u ∈ G.V - {s} 2 u.color = WHITE; u.d = ∞; u.π = NIL 3 s.color = GREY; s.d = 0; s.π = NIL 4 Q = ∅ 5 ENQUEUE(Q, s) 6 while Q ≠ ∅ 7 u = DEQUEUE(Q) 8 for each v ∈ G.Adj[u] 9 if v.color == WHITE // first time we have seen v 10 v.color = GREY 11 v.d = u.d + 1 12 v.π = u 13 ENQUEUE(Q, v) 14 u.color = BLACK

O(V + E). Each vertex is enqueued at most once, and each adjacency list is scanned once.

What BFS computes

Theorem 20.5 (correctness of BFS). On termination, v.d = δ(s, v) for every vertex v, where δ is the shortest-path distance measured in number of edges. Moreover the predecessor pointers π form a breadth-first tree in which the unique path from s to v is a shortest path.

Two supporting facts:

BFS gives shortest paths only when all edges have the same weight. It counts edges, not weight. For weighted graphs you need Dijkstra (Chapter 22), which is BFS with a priority queue substituted for the plain queue — a substitution worth noticing, because it is the whole difference.

Depth-first search

DFS goes as deep as possible before backtracking, and unlike BFS it searches from every vertex, producing a depth-first forest rather than a single tree.

DFS(G) DFS-VISIT(G, u) 1 for each u ∈ G.V 1 time = time + 1 2 u.color = WHITE 2 u.d = time // discovered 3 u.π = NIL 3 u.color = GREY 4 time = 0 4 for each v ∈ G.Adj[u] 5 for each u ∈ G.V 5 if v.color == WHITE 6 if u.color == WHITE 6 v.π = u 7 DFS-VISIT(G, u) 7 DFS-VISIT(G, v) 8 u.color = BLACK 9 time = time + 1 10 u.f = time // finished

Θ(V + E). Every vertex gets two timestamps: u.d when first discovered (turns grey) and u.f when its adjacency list is exhausted (turns black). Timestamps run from 1 to 2|V|.

Timestamps and the parenthesis theorem

The timestamps are not incidental — they carry the structure that the next two algorithms exploit.

Theorem 20.7 (parenthesis theorem). In any depth-first search, for any two vertices u and v, exactly one of the following holds: the intervals [u.d, u.f] and [v.d, v.f] are entirely disjoint, and neither is a descendant of the other; or one interval is entirely contained in the other, and the inner vertex is a descendant of the outer.

Write out the discovery and finish times as a sequence of parentheses and they nest properly, exactly like well-formed brackets. Partial overlap never happens.

Theorem 20.9 (white-path theorem). Vertex v is a descendant of u in the depth-first forest if and only if, at the time u.d when the search discovers u, there is a path from u to v consisting entirely of white vertices.

Edge classification

DFS classifies every edge (u,v) by the colour of v when the edge is first explored.

Typev isMeaning
Tree edgeWHITEv was discovered by this edge; it is in the DFS forest.
Back edgeGREYv is an ancestor of u. Indicates a cycle.
Forward edgeBLACK, u.d < v.dv is a descendant, reached earlier by another route.
Cross edgeBLACK, u.d > v.dEverything else — between subtrees or between trees.
Theorem 20.11. A directed graph is acyclic if and only if a depth-first search of it yields no back edges. This single test is the basis of cycle detection, and it costs nothing beyond the DFS itself. In an undirected graph, DFS produces only tree and back edges — forward and cross edges cannot occur.

Topological sort

A topological sort of a directed acyclic graph is a linear ordering of the vertices such that every edge (u,v) has u before v. This is the “do the prerequisites first” ordering: build dependencies, course prerequisites, task scheduling, spreadsheet recalculation.

TOPOLOGICAL-SORT(G) 1 call DFS(G) to compute finish times v.f for each vertex v 2 as each vertex is finished, insert it onto the front of a linked list 3 return the linked list of vertices

Θ(V + E). That is the entire algorithm: DFS, and output in decreasing order of finish time.

Why it works (Theorem 20.12). For any edge (u,v) in a DAG, DFS gives v.f < u.f. If v is white when (u,v) is explored it becomes a descendant and finishes first; if it is black it already finished; and it cannot be grey, since that would make (u,v) a back edge and the graph would have a cycle. So sorting by decreasing finish time puts every u before every v.

The alternative algorithm, Kahn’s, repeatedly removes a vertex of in-degree zero. Also Θ(V+E), easier to implement iteratively, and it detects cycles naturally by leaving vertices behind. CLRS gives the DFS version because it reuses machinery already built.

Strongly connected components

A strongly connected component of a directed graph is a maximal set of vertices such that every pair u, v in it has a path from u to v and a path from v to u. Contracting each SCC to a single vertex yields the component graph, which is always a DAG.

The algorithm is startlingly short given what it computes.

STRONGLY-CONNECTED-COMPONENTS(G) 1 call DFS(G) to compute finish times u.f for each vertex u 2 compute Gᵀ, the transpose of G (all edges reversed) 3 call DFS(Gᵀ), but in the main loop consider vertices in order of decreasing u.f as computed in line 1 4 output the vertices of each tree in the depth-first forest of line 3 as a separate strongly connected component

Θ(V + E) — two depth-first searches and one transpose. This is Kosaraju’s algorithm.

abc def {a,b,c} {d,e} {f} component graph abcdef always a DAG
Figure 20.1 — Strongly connected components and the DAG they form when contracted.
Why transposing works. G and Gᵀ have exactly the same strongly connected components, since mutual reachability is symmetric under edge reversal. Processing Gᵀ in decreasing finish order visits the components in topological order of the component DAG, and in the transposed graph the edges between components point the wrong way — so each DFS tree cannot escape its own component. That containment is what makes each tree exactly one SCC.

Recap

The nine things to carry forward

Where this goes next

Chapter 21 adds weights and asks for a minimum spanning tree: the cheapest set of edges connecting every vertex. Both standard algorithms, Kruskal’s and Prim’s, are greedy, and both are instances of one generic method justified by a single theorem about safe edges.


Ch 19 — Data Structures for Disjoint Sets Ch 21 — Minimum Spanning Trees