Strongly Connected Components
Consider a network of web pages linked by hyperlinks, or a set of towns connected by one-way roads. Some groups of pages link to each other so thoroughly that you can browse from any page in the group back to any other. Other pages sit in one-way chains that you can never return from. Detecting these mutually-reachable clusters is the problem of finding strongly connected components (SCCs). It is one of the cleanest applications of depth-first search to a genuinely structural question about directed graphs. SCCs matter because directed graphs are rarely as tidy as trees or DAGs. Compiler dependency graphs, deadlock-detection graphs in operating systems, citation networks, and the call graphs of recursive functions all contain cycles. DFS alone does not resolve these cycles into a hierarchy. Collapsing each strongly connected component into a single supernode always produces a directed acyclic graph. This is called the component graph, and it can then be topologically sorted, scheduled, or analyzed layer by layer. This collapsing step turns a messy cyclic graph into a clean DAG. It is the same DAG structure that underlies build systems, spreadsheet recalculation, and course-prerequisite planning. This article presents Kosaraju's algorithm, a beautifully economical method. It computes all SCCs using exactly two depth-first searches. One search runs on the graph , the other on its transpose (the graph with every edge reversed). The two-pass structure is not a coincidence. It is forced by a precise theorem about how DFS finish times interact with the component graph. Understanding that theorem is the real payoff of studying this algorithm. By the end of this article you will be able to run Kosaraju's algorithm by hand. You will prove that it is correct using the component-DAG argument. You will analyze its running time. You will also recognize the classic implementation traps. This especially includes the finish-time ordering mistake that silently produces wrong answers on graphs that look correct at first glance.
- A strongly connected component (SCC) is a maximal set of vertices where every vertex can reach every other vertex.
- Collapsing each SCC into one node always yields a DAG: the component graph .
- Kosaraju's algorithm: DFS on to get finish times, then DFS on processing vertices in decreasing finish-time order.
- Each DFS tree produced in the second pass is exactly one SCC — this is the theorem to prove, not just believe.
- Running time is : two linear-time DFS passes plus one linear-time transpose construction.
- The classic bug is running the second DFS in the wrong order (increasing instead of decreasing finish time), which silently breaks correctness.
Running DFS on to rank vertices by finish time, then DFS on in that ranked order, forces each resulting DFS tree to align exactly with one strongly connected component.
The idea
A directed graph has vertices and that are strongly connected if there is a path from to and a path from to . Strong connectivity is an equivalence relation on : it is reflexive (a vertex trivially reaches itself), symmetric by definition, and transitive (if and then by concatenating paths). Because it is an equivalence relation, it partitions into disjoint equivalence classes — these classes are the strongly connected components of . Each SCC is a maximal subset of vertices that are pairwise mutually reachable. Given the SCCs , define the component graph by contracting each to a single supernode, and drawing an edge whenever some edge in goes from a vertex in to a vertex in (for ). A short argument shows is always acyclic: if had a cycle through supernodes , then every vertex in every could reach every vertex in every other through this cycle, forcing them all into one SCC — contradicting that the were distinct maximal components. This acyclicity is exactly why Topological Sort applies to the component graph once it is built, and why SCC decomposition is often the first preprocessing step before running DAG-based scheduling algorithms. Kosaraju's algorithm exploits DFS Edge Classification and Timestamps with a clever twist: it runs DFS twice, once forward on and once backward on the transpose graph (same vertex set, every edge reversed). Note that and have exactly the same SCCs, since reversing every edge reverses every path, so mutual reachability is preserved. The key insight — proved in the next section — is that if you process vertices in the second DFS in decreasing order of their finish times from the first DFS, each resulting DFS tree in the forest is precisely one SCC.
How it works
The algorithm has three phases: a DFS on recording finish times, a transposition of the graph, and a DFS on visiting vertices in decreasing finish-time order.
Phase 1. Run DFS on . Every time a vertex's adjacency list is exhausted (its DFS call is about to return), stamp it with a finish time and push it onto a stack (or append to a list). The vertex finishing last ends up on top.
Phase 2. Build by reversing every edge of . If is given as adjacency lists, this takes time: scan every edge once and insert into 's adjacency list.
Phase 3. Run DFS on , but choose the next unvisited root strictly in decreasing order of the Phase 1 finish times (equivalently, pop vertices off the stack built in Phase 1). Each tree produced by this DFS forest is output as one strongly connected component. ``` KOSARAJU-SCC(G): // Phase 1: DFS on G, record finish order for each vertex u in G.V: u.visited = false finishOrder = empty stack for each vertex u in G.V: if not u.visited: DFS-VISIT-1(G, u, finishOrder) // Phase 2: build transpose G_T = TRANSPOSE(G) // Phase 3: DFS on G_T, in decreasing finish order for each vertex u in G_T.V: u.visited = false components = empty list while finishOrder is not empty: u = finishOrder.pop() if not u.visited: newComponent = empty set DFS-VISIT-2(G_T, u, newComponent) components.append(newComponent) return components
DFS-VISIT-1(G, u, finishOrder): u.visited = true for each v in G.adj[u]: if not v.visited: DFS-VISIT-1(G, v, finishOrder) finishOrder.push(u) // stamp finish time by push order
DFS-VISIT-2(G_T, u, component): u.visited = true component.add(u) for each v in G_T.adj[u]: if not v.visited: DFS-VISIT-2(G_T, v, component)
TRANSPOSE(G): G_T = new graph with same vertices as G, no edges for each vertex u in G.V: for each v in G.adj[u]: G_T.adj[v].add(u) return G_T ``` Every vertex is visited exactly once in each DFS pass, and every edge is examined exactly once per pass (once in , once in ), so both DFS phases and the transpose construction are linear in the size of the graph.
The diagram shows the essential shape: has three SCCs (the blue triangle , the orange triangle , and the singleton ), the component graph collapsing them is a simple chain (hence acyclic), and the finish stack from Phase 1 always places a vertex of the "last" SCC in the topological order (, the source of the DAG) on top, guaranteeing it is popped first in Phase 3.
Complexity & correctness
Running time. Let and . Phase 1 is a standard DFS: . Building scans every edge once: . Phase 3 is another standard DFS over , which has the same number of vertices and edges as : . Summing three linear phases gives total time
which is asymptotically optimal — any correct algorithm must at least read the input. Space is for the adjacency lists of and , plus for the finish-time stack, visited flags, and recursion stack.
Correctness — the component-DAG argument. The proof rests on one key lemma about DFS finish times applied to the component graph. Lemma (finish-time ordering across components). Let and be distinct SCCs of , and suppose there is an edge from some vertex of to some vertex of in (i.e., ). Let denote the maximum finish time, over a DFS of , of any vertex in . Then
Proof sketch. Consider whichever of or is discovered first by the DFS, and let be the first vertex discovered among .
- Case 1: . Since and (because is acyclic) there is no edge back from to , every vertex of reachable from is reached via some path leaving that never returns. At the moment is discovered, all of and all of reachable from are still white, so by the white-path property of DFS, 's DFS subtree will finish after completely exploring both and (since is reachable from and hence from before finishes). Hence , and therefore some vertex of , finishes after every vertex of : .
- Case 2: . Then since there is no edge from back into (acyclicity of again), DFS from cannot reach any vertex of . So the entire component is discovered and finished — including the vertex achieving — strictly before DFS ever discovers any vertex of (because was the first vertex found in , so vertices are still white when finishes). Hence every vertex of finishes later, giving again.
In both cases , proving the lemma. Consequence. This lemma says that if we rank the SCCs by their maximum finish time from a DFS of , that ranking is exactly a reverse topological order of — every edge has , so the component with globally largest finish time is a source of (nothing points into it from another component, since anything pointing in would have to finish even later). Why the second pass then works. In Phase 3 we run DFS on , and (the component graph of the transpose) is exactly with every edge reversed. So the component with the largest -value — a source in — becomes a sink in : no edges leave it in the transposed component graph. We start Phase 3 at a vertex of that component (it sits on top of the finish stack). Since it is a sink in , DFS from it in cannot escape to any other component — the search explores exactly the vertices of that one SCC (they are mutually reachable within the component, and reachability confirms it can't leak out). So the first DFS tree grown in Phase 3 is exactly that one SCC. Now remove that finished component and repeat the argument inductively: among the remaining unvisited components, whichever has the next-highest value is a source in the remaining subgraph of , hence a sink in the remaining subgraph of , and it sits highest on the still-unpopped part of the stack. So Phase 3's next DFS tree, rooted there, again cannot escape its own component. By induction, every DFS tree produced in Phase 3 is exactly one SCC, and every vertex is assigned to exactly one tree (since Phase 3 is a complete DFS forest over all vertices of ). This proves Kosaraju's algorithm partitions correctly into SCCs.
Tradeoffs. Kosaraju's algorithm is conceptually the simplest linear-time SCC algorithm to prove correct, because the proof reduces to one clean lemma about finish times and component-DAG structure. Its practical downside is needing to materialize explicitly and store two full adjacency structures, and needing two full DFS passes with an intervening full graph traversal to sort by finish time. Tarjan's single-pass algorithm and the path-based Gabow algorithm achieve the same bound with one DFS pass and no explicit transpose, at the cost of a more intricate low-link invariant that is harder to prove correct from first principles — Kosaraju's algorithm is usually taught first precisely because its proof is so self-contained.
Worked example
Take the graph from the diagram above with vertices and edges:
Phase 1 — DFS on , starting at (assume adjacency lists list neighbors alphabetically):
| Step | Action | Stack pushed (finish order) |
|---|---|---|
| 1 | Visit , go to | |
| 2 | Visit , go to | |
| 3 | Visit , neighbor visited, go to | |
| 4 | Visit , go to | |
| 5 | Visit , go to | |
| 6 | Visit , neighbor visited (no white neighbors) → finish | push |
| 7 | Back to , no more neighbors → finish | push |
| 8 | Back to , next neighbor , visit | |
| 9 | has no outgoing edges → finish | push |
| 10 | Back to , no more neighbors → finish | push |
| 11 | Back to , no more neighbors → finish | push |
| 12 | Back to , no more neighbors → finish | push |
| 13 | Back to , no more neighbors → finish | push |
Stack from bottom to top: . Popping gives the order .
Phase 2 — Transpose. Reversing every edge gives : .
Phase 3 — DFS on in the order (popped from the stack):
| Pop | Visited already? | DFS-tree grown | Resulting SCC |
|---|---|---|---|
| no | ( visited, back to : , 's only edge visited) | ||
| yes | — | — | |
| yes | — | — | |
| no | (visited, skip) ( visited); no escape to or to | ||
| no | (already visited, skip) | ||
| yes | — | — | |
| yes | — | — |
Output: three SCCs, , , — matching the diagram exactly, and matching the ground truth since form a 3-cycle, form a 3-cycle, and has no outgoing or return path. Notice the crucial detail: even though is an edge in , when Phase 3 reaches the popped vertex and explores , the edge in is , not — so DFS from never reaches , correctly keeping isolated as its own SCC, and confirming the component graph really is the acyclic chain .
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Running Phase 3 in increasing finish-time order | Confusing "process by finish time" with "smallest first," as in some greedy algorithms | Always pop from the top of the finish stack (largest finish time first) — verify against the proof's reverse-topological argument |
| Forgetting to reset `visited` flags between Phase 1 and Phase 3 | Reusing the same visited array for both DFS passes | Use a fresh visited array (or reset it) before the second DFS |
| Building incorrectly (e.g. reversing only some edges or leaving self-loops out) | Off-by-one style bugs when writing the transpose loop, or overlooking multi-edges | Iterate every adjacency list of exactly once, insert the reverse edge, and mirror any self-loops as-is |
| Treating a single DFS on alone as sufficient | Believing finish order alone identifies SCCs without the transpose pass | Remember: finish order gives a valid ordering of the component DAG, not the components themselves — the transpose pass is what isolates each component |
| Off-by-one in stack indexing when implementing iteratively | Manual stack management for recursion-free DFS can mishandle push/pop timing relative to "finish" | Only mark a vertex "finished" (and push it) after all its neighbors have been fully explored, not when it is first discovered |
| Assuming SCCs are contiguous or singletons in sparse-looking graphs | Visual intuition from small examples | Always verify SCCs are maximal via the mutual-reachability definition, not visual grouping |
Pioneers & history
Kosaraju's algorithm is named for S. Rao Kosaraju, who described the two-pass DFS-on-transpose technique in an unpublished 1978 note; the same idea was independently rediscovered and popularized by Micha Sharir in 1981, and it is sometimes called the Kosaraju–Sharir algorithm. The technique sits alongside Robert Tarjan's 1972 single-pass low-link algorithm as one of the two classic linear-time methods for SCC decomposition, both building directly on the DFS discovery/finish-time framework that Tarjan himself had systematized for reasoning about graph structure.
Origins · 1 minRobert Endre Tarjan and how this was discovered
Tarjan developed the DFS-based low-link technique for strongly connected components and biconnected components in the early 1970s, and later co-invented the union-find analysis, splay trees, and Fibonacci heaps, establishing much of the toolkit modern graph algorithms still rely on.
Where this matters
Compilers and build systems use SCC decomposition to find groups of mutually recursive functions or circular module dependencies before doing dependency-ordered compilation; each SCC in the call graph must be compiled or type-checked as a unit, and the acyclic component graph then gives a valid compilation order via Topological Sort. Package managers and dependency resolvers (in language ecosystems and container-build tools) use the same idea to detect cyclic dependencies that must be flagged to the user rather than silently ordered. Deadlock detection in operating systems and database systems models resource-allocation graphs as directed graphs, where a cycle among "waiting-for" edges signals a deadlock; SCC computation finds all such cycles simultaneously rather than searching for one cycle at a time. Web-scale graph analysis (search-engine crawling, citation-network research, social-network community structure) uses SCC decomposition to identify the "core" of mutually-linking pages versus pages that are only ever linked to or only ever link out, a structure famously described in large-scale studies of the web graph's "bowtie" shape. Model checking and static analysis tools that build control-flow or state-transition graphs use SCCs to isolate loops and recursive structures for separate fixpoint or termination analysis.
Ask the tutor: How does Tarjan's single-pass low-link algorithm find the same SCCs without ever building the transpose graph?
Why does Kosaraju's algorithm need two separate DFS passes instead of one?
A single DFS on produces a valid topological-style ordering of the component graph (via the finish-time lemma), but it does not by itself separate vertices into components — a single DFS tree can span multiple SCCs whenever one component reaches another. The second pass, on the transpose graph and in the finish-time order discovered by the first pass, exploits the fact that reversing edges turns each "source" component in the first pass's ordering into a "sink" component with no outgoing edges to other unvisited components. That sink property is what prevents the second DFS from leaking outside a single SCC, so the two passes together — not either alone — are what isolate the components.
Is Kosaraju's algorithm the fastest way to find strongly connected components?
Asymptotically, no algorithm beats for SCCs since that is linear in the input size, and Kosaraju's algorithm already achieves this bound — so it is asymptotically optimal, tied with Tarjan's algorithm and the Gabow path-based algorithm. In practice, Tarjan's single-pass algorithm is often preferred because it avoids explicitly constructing the transpose graph and needs only one DFS traversal with a small amount of extra per-vertex bookkeeping (a low-link value). Kosaraju's algorithm is usually preferred pedagogically because its correctness proof is shorter and more modular, resting on one clean lemma about finish times.
Does Kosaraju's algorithm work on undirected graphs?
It is designed for directed graphs; on an undirected graph, "strongly connected" and "connected" coincide, since every edge is trivially traversable both ways, and a single DFS or BFS pass already finds ordinary connected components in time. Running Kosaraju's two-pass machinery on an undirected graph still gives the correct answer (each connected component becomes one SCC) but is wasteful, since the transpose of an undirected graph (as a symmetric directed graph) is identical to the graph itself, making the second pass redundant.
What happens if the graph has self-loops or multiple edges between the same pair of vertices?
Self-loops do not affect SCC membership: a vertex with a self-loop is still just one vertex, trivially strongly connected to itself regardless of the loop, so DFS treats it the same as a vertex without one (the loop edge is simply skipped since the vertex is already visited). Multiple parallel edges between the same ordered pair of vertices also do not change the SCC structure, since Kosaraju's algorithm only cares about reachability, not edge multiplicity; a good implementation should still be careful when building the transpose to preserve (or safely deduplicate) parallel edges depending on whether edge weights or counts matter downstream.
Can Kosaraju's algorithm be implemented iteratively instead of recursively?
Yes, and for large graphs this is often necessary to avoid recursion-depth limits (a long directed path can cause recursion depth). Both DFS passes can be rewritten using an explicit stack that simulates the call stack, being careful to only mark a vertex as "finished" (and push it to the finish-order stack in Phase 1) after all of its neighbors have been processed, not merely upon first visiting it. This iterative rewrite does not change the asymptotic complexity, since it still visits every vertex and edge exactly once per pass.
How is the component graph G^SCC actually constructed once SCCs are known?
After Phase 3 assigns every vertex to a component (e.g., by giving it a component ID equal to the index of the DFS tree that contains it), scan every original edge of once: if and have different component IDs, add an edge between those two component IDs in (deduplicating parallel edges if desired). This scan takes time on top of the already spent finding the components, so building the full component DAG remains linear overall, ready for Topological Sort or further DAG-based processing.
What role do DFS discovery and finish timestamps play beyond this algorithm?
Discovery and finish timestamps, as introduced when studying DFS Edge Classification and Timestamps, are the same bookkeeping device used to classify edges as tree, back, forward, or cross edges, and to detect cycles (a back edge implies a cycle in a directed graph). Kosaraju's algorithm is a striking demonstration that these timestamps carry global structural information — specifically, a valid reverse-topological ranking of the component graph — not just local information about a single DFS tree, which is why the same timestamp machinery reappears across many graph algorithms.