Topological Sort and Strong Connectivity
Directed acyclic graphs model dependencies: tasks that must precede other tasks. A topological order lays such a graph out in a line so every edge points forward, and depth-first finish times yield one almost for free.
╌╌╌╌
Many problems are really questions about order. To compile a program you must
build each module before the ones that depend on it; to follow a recipe you must
chop before you sauté; to finish a degree you must clear the prerequisites of
each course. Each of these is a directed acyclic graph, a digraph with no
directed cycles, and the task of find a consistent order
is topological
sorting. Depth-first search,
with its finish-time timestamps from the previous lesson, solves it almost
incidentally.
Directed acyclic graphs
The absence of cycles is what makes a consistent ordering possible: if
task must precede and must precede , no linear order can satisfy
both. Here is a small DAG of course prerequisites, where an edge means
must come before
:
Topological order
Picture all the vertices pinned along a horizontal line so that every edge points rightward. The DAG above admits the order ; each of its six edges goes left to right:
Topological orders are usually not unique; works equally well. Two facts tie ordering to acyclicity:
Topological sort via DFS finish times
All three texts draw out the same observation, linking acyclicity to depth-first search's edge classification from the previous lesson:1
Since DAGs have no back edges, every edge of a DAG is a tree, forward, or cross edge, and for all three of those, the finish times obey . That single inequality gives the algorithm:
So we run DFS, and as each vertex finishes we push it onto the front of a list. When DFS completes, the list reads off a valid topological order.2
- 1empty linked list
- 2foreach vertex do
- 3
- 4foreach vertex do
- 5if then
- 6call
- 7return
- 1
- 2foreach adjacent to do
- 3if then
- 4call
- 5
- 6prepend to the front ofsmaller finish goes later
Running time. This is just DFS plus work per vertex to splice it into the list, so it runs in , which is linear.
A full trace on the prerequisite DAG
Run the algorithm on the prerequisite DAG, starting at and taking 's adjacency list in the order (adjacency-list order is arbitrary; this one keeps the trace short). Every discovery and finish ticks the same clock, so the run is a sequence of ten timestamped events:
| time | event | call stack | afterwards |
|---|---|---|---|
| 1 | discover | ||
| 2 | discover | ||
| 3 | discover | ||
| 4 | discover | ||
| 5 | finish | ||
| 6 | finish | ||
| 7 | finish | ||
| 8 | discover | ||
| 9 | finish | ||
| 10 | finish | — |
At time 8, inspects its neighbors and , finds both black (finished), and finishes immediately — those two edges become cross edges, and the inequality , holds for them just as it does for tree edges. The final list is the topological order, and sorting the vertices by decreasing reproduces it exactly. Every edge, drawn below the sorted line, points rightward:
Why a topological order matters: the evaluation DAG
For example, take computing Fibonacci numbers . The naive recursion branches into and , and its call tree is exponential, but most of its nodes are duplicates. If we collapse the identical subproblems into single nodes, the recursion tree becomes a small DAG: one node per value , with an edge whenever computing needs .
To compute we must evaluate every node after the nodes it points to are already known, that is, in a valid topological order of the evaluation DAG. Here the order is read off by level: . Filling an array in that order computes each value with work, turning exponential recursion into a single linear sweep:
- 1allocate array
- 2;
- 3for to do
- 4predecessors already done
- 5return
The lesson generalizes: whenever quantities depend on one another acyclically, their dependency digraph is a DAG, and a topological order gives a safe order in which to evaluate them, predecessors first. This is the structure underlying dynamic programming, which we return to later.
Kahn's algorithm: peeling sources
Skiena presents the equivalent Kahn's algorithm, which never mentions finish times.3 The idea is an induction on sources. A DAG always has at least one vertex of in-degree (follow edges backward from any vertex; with no cycle the walk must stop, and it stops at a source). Any source can safely go first in the order, and deleting it leaves a smaller DAG, so repeat.
- 1compute for everyone pass over all adjacency lists
- 2queue of all vertices with
- 3empty list
- 4while is nonempty do
- 5dequeue ; append to
- 6foreach adjacent to do
- 7delete 's out-edges
- 8if then enqueue
- 9if then report "cycle" else return
The deletion is virtual: decrementing stands in for removing the edge . On the prerequisite DAG the in-degrees start at , , , , , and the run proceeds (breaking queue ties alphabetically):
| step | emit | decrements | in-degrees left () | queue after |
|---|---|---|---|---|
| 1 | , | |||
| 2 | , | |||
| 3 | ||||
| 4 | ||||
| 5 | — | done | empty |
The result, , happens to match the DFS order; with a different tie-break ( before at step 1) it would produce the equally valid . Two properties fall out of the loop structure:
- Cycle detection is free. If the queue empties while vertices remain, every leftover vertex has in-degree among the leftovers, and following in-edges backward inside that set forever must revisit a vertex: the leftovers contain a cycle. So if and only if is not a DAG — Kahn's algorithm doubles as a cycle detector.
- Counting the orders. Whenever the queue holds vertices, any of the may go next; the algorithm enumerates one topological order per tie-break policy, and swapping the queue for a priority queue produces the lexicographically smallest order at cost.
Running time. Computing all in-degrees touches every edge once, . Each vertex is enqueued and dequeued at most once (), and each edge triggers exactly one decrement, when is emitted (). Total: , matching the DFS method.
Strong connectivity
DAGs are the cycle-free case. What can we say about a general digraph, cycles
and all? The right notion of connected
for directed graphs is mutual
reachability.
Strong connectivity partitions into SCCs. Collapsing each component to a single super-vertex yields the component graph (or condensation), and the following holds:
So every directed graph is, at the coarse level of its components, a DAG. SCCs are the standard first step in analyzing a digraph: find the components, contract them, and reason about the resulting DAG.
Here form one SCC (each reaches the other) and another, and the only edges between the two groups run from to . Collapsing each component to a super-vertex leaves the two-node condensation, itself a DAG, with its own trivial topological order then :
Kosaraju's two-pass algorithm
The cleanest way to find SCCs, due to Kosaraju and Sharir, is two depth-first searches with a transpose in between.3 The transpose is with every edge reversed. It has exactly the same SCCs as : a round trip in becomes the round trip in traversed the other way, so mutual reachability is untouched.
- 1call to compute the finish time for each vertex
- 2computereverse all edges
- 3call , considering vertices in order of decreasing
- 4output the vertices of each tree in the second forest as one SCC
Why it works (the intuition). Imagine the component graph laid out in topological order, sources on the left. The first DFS on assigns the largest finish time to a vertex in a source component of that DAG. When we then run DFS on , where every component-graph edge is reversed, and start from that highest-finishing vertex, we are launching from a sink of the reversed component graph. From a sink, the search cannot leak into any other component, so it visits exactly one SCC and stops. Peeling components off in decreasing finish order keeps this true at every step. The whole procedure is two DFS passes plus a transpose, all linear, so SCCs cost .
The picture below shows the source/sink reversal. The first DFS on a graph with three SCCs lands the largest finish time () inside the source component ; reversing the edges turns that source into a sink, so the second DFS, launched from , is trapped inside one SCC and peels it off cleanly:
The intuition hardens into two short proofs. Write for the largest first-pass finish time inside component .
The lemma says the first pass computes, for free, a reverse topological order of the condensation: listing components by decreasing lists them source to sink. The second pass exploits it.
A complete run
Here is the full machinery on an eight-vertex digraph adapted from CLRS's worked example.2 Its components are , , , and . Pass 1 runs DFS on from with alphabetical adjacency lists and records finish times: starts a tree at time and the exploration order is , giving the finish times shown below.
Reading the vertices by decreasing finish time gives the processing order for pass 2:
Pass 2 reverses every edge and launches DFS roots in that order. Each root grows a tree, and each tree is one SCC:
| root | reason it starts a tree | tree grown in | SCC found |
|---|---|---|---|
| largest overall | , | ||
| largest still unvisited () | |||
| next unvisited () | |||
| next unvisited () | (no unvisited neighbor) |
Each tree halts at the border of its own component. Every reversed edge that leaves a tree — from the second, and from the third, and from the last — lands on an already-visited vertex, because it exits toward a component with larger , which the decreasing- schedule has already peeled. The first tree needs no such luck: a source component has no incoming edges in , hence no outgoing edges in , so the search from is walled in from the start:
Contracting the four components produces the condensation, and the maxima read off a topological order of it, as the lemma guarantees:
Running time, in full. Pass 1 is one DFS: . No sorting is needed to order vertices by decreasing finish time — push each vertex onto a stack as it finishes and pop the stack in pass 2, . Building is one scan of the adjacency lists: for each and each , append to , which is . Pass 2 is another DFS, . The sum is three linear passes plus a stack:
Common pitfalls
- Sorting by discovery time instead of finish time. The two are not interchangeable. On the three-vertex DAG with edges , , , a DFS from that tries first discovers vertices in the order — and that order violates the edge . Finish times ( first, then , then , reversed to ) are what the theorem guarantees.
- Appending instead of prepending. pushes each finished vertex onto the front of ; appending to the back builds the exact reverse of a topological order. The stack formulation avoids the confusion: push on finish, then pop everything.
- Forgetting the outer loop. Both DFS passes must restart from every still-white vertex, not just one chosen source. A DAG can have several sources, and in Kosaraju's second pass the restarts are the whole point — each restart begins a new component.
- Running toposort on a cyclic graph without checking. DFS finish times always produce an ordering, even on a cyclic input, where no valid order exists; garbage in, garbage out. Detect the cycle first: a gray-to-gray edge in DFS, or leftover vertices in Kahn's algorithm.
- Reversing the wrong thing in Kosaraju. The second pass runs on in decreasing finish order of the first pass. Increasing order breaks the invariant that each root's component is a source among the survivors. (The mirror-image variant — first pass on , second on — is fine, since .)
- Treating SCCs like undirected components. One directed path between two vertices does not make them strongly connected; the path back must also exist. A digraph can be weakly connected (connected if you ignore directions) yet have singleton SCCs — any DAG is an example.
Condensations and one-pass SCC
Tarjan's one-pass algorithm. Kosaraju runs DFS twice; Tarjan's algorithm (1972) finds SCCs in a single pass.4 It carries a low[v] value — the smallest discovery time reachable from 's subtree via at most one back or cross edge into the current stack — exactly the low-link idea reused in the bridges and articulation points lesson. Vertices are pushed onto an auxiliary stack as they are discovered; when a vertex finishes with low[v] == disc[v], it is the root of an SCC, and everything above it on the stack is popped off as that component. One DFS, no transpose graph, and the components emerge in reverse topological order for free — which is why competitive-programming 2-SAT solvers almost always use Tarjan.
The condensation is the point. Collapsing each SCC to a super-vertex yields the condensation , always a DAG. Many is there a path / can everything reach everything
questions on a general digraph reduce to a topological-order sweep over this DAG: reachability, computing the transitive closure component-wise, finding a single vertex that reaches all others (a source SCC in the condensation), or adding the fewest edges to make a digraph strongly connected (a classic result of Eswaran and Tarjan counts sources and sinks of the condensation). The two-phase pattern — find SCCs, then run a DAG algorithm on the condensation — is the template behind the whole next stretch of this module, most directly 2-SAT.
Dynamic and incremental variants. When edges arrive over time, recomputing SCCs from scratch is wasteful; incremental-SCC and incremental-topological-order algorithms (Bender, Fineman, Gilbert, Tarjan) maintain the ordering under edge insertions in near-linear total time, the machinery behind pointer-analysis and build-system dependency engines that must react to each new edge.
Takeaways
- A DAG is a directed graph with no cycle; it has a topological order (every edge points forward) if and only if it is acyclic.
- DFS detects acyclicity by the absence of back edges, and listing vertices in decreasing finish time yields a topological order in .
- Kahn's algorithm peels off in-degree- sources with a queue, also in , and detects a cycle for free: the queue runs dry with vertices left over exactly when the graph is not a DAG.
- A topological order provides a safe evaluation order for acyclically dependent quantities, predecessors first. Collapsing the Fibonacci recursion into its evaluation DAG and sweeping it in topological order turns exponential recursion into a linear pass, the seed of dynamic programming.
- Strongly connected components are maximal mutually-reachable vertex sets; contracting them always produces a DAG.
- Kosaraju's two-pass DFS (run DFS, transpose, run DFS in decreasing finish order) finds all SCCs in ; Tarjan's low-link method does it in one pass.
Footnotes
- Erickson, Ch. 6 — Depth-First Search — a digraph is acyclic iff DFS finds no back edge. ↩
- CLRS, Ch. 22 — Elementary Graph Algorithms — topological sort by decreasing DFS finish time in . ↩ ↩2
- Skiena, §5 — Graph Traversal — finding strongly connected components via two DFS passes. ↩ ↩2
- Tarjan, R. E. (1972),
Depth-first search and linear graph algorithms,
SIAM Journal on Computing 1(2), 146–160 — the single-pass low-link SCC algorithm. ↩
╌╌ END ╌╌