A Bit of History
This is the third algorithm in this series traced to Robert Tarjan's single, extraordinarily productive 1972 paper "Depth-First Search and Linear Graph Algorithms" — alongside bridges and articulation points, already met in Part 8. All three share the exact same core idea: a low-link value computed during one DFS pass, repurposed slightly differently for each problem. Tarjan's SCC algorithm is arguably the cleverest application of the three, folding an entire two-pass strategy (Kosaraju's, published around the same era) into a single traversal.
Working Principle
Tarjan's algorithm runs one DFS, maintaining for each vertex \(v\) a discovery index \(\text{disc}[v]\) (the order it was first visited) and a low-link value \(\text{low}[v]\) — the smallest discovery index reachable from \(v\)'s subtree using at most one back edge or cross edge to a vertex still "on the stack" (i.e., part of the current SCC search still in progress). A second stack tracks every vertex currently being explored. When a vertex \(v\) finishes and \(\text{low}[v] = \text{disc}[v]\) (meaning nothing in \(v\)'s subtree can reach back above \(v\)), \(v\) is the root of a complete SCC: pop the stack until \(v\) itself is popped, and every vertex popped along the way belongs to that SCC.
Analogy: Rock Climbers on a Shared Rope
Picture DFS as a chain of climbers, each one's rope anchored to the climber before them. The stack represents everyone still "roped in" to the current climb. A climber's low-link value is the highest point on the mountain (lowest discovery index) that someone still below them on the rope could theoretically reach by a side-path. The moment a climber realizes nobody below them can reach any point higher than where they themselves started, that climber and everyone still roped in below them form one self-contained group — exactly one SCC — and can be safely "cut loose" from the rest of the climb.
Worked Example
The same graph as the Kosaraju's deep dive: 3-cycle \(A \to B \to C \to A\), 2-cycle \(D \to E \to D\), bridge edge \(C \to D\). Starting DFS at \(A\): \(\text{disc}[A]=0\), visit \(B\) (\(\text{disc}=1\)), visit \(C\) (\(\text{disc}=2\)), which has an edge back to \(A\) (still on the stack) — so \(\text{low}[C] = \min(\text{low}[C], \text{disc}[A]) = 0\). \(C\) also visits \(D\) (\(\text{disc}=3\)) and \(E\) (\(\text{disc}=4\), which links back to \(D\), giving \(\text{low}[E] = \text{low}[D] = 3\)). When \(D\) finishes, \(\text{low}[D] = 3 = \text{disc}[D]\) — pop the stack down to \(D\): SCC \(\{D, E\}\) found. Backtracking further, when \(A\) finishes, \(\text{low}[A] = 0 = \text{disc}[A]\) — pop down to \(A\): SCC \(\{A, B, C\}\) found. Same two SCCs as Kosaraju's algorithm found, in a single pass.
Why Low-Link Values Work
The correctness argument mirrors the bridge-finding argument from Part 8, adapted for strong connectivity: \(\text{low}[v] = \text{disc}[v]\) after \(v\)'s subtree is fully explored means no vertex in that subtree has a back or cross edge to any vertex discovered before \(v\) that is still active (on the stack). Combined with the fact that every vertex still on the stack when \(v\) finishes is, by construction, reachable from \(v\) (they were pushed while exploring \(v\)'s subtree) and can reach back to \(v\) (else they wouldn't still be on the stack) — the set popped is exactly the maximal mutually-reachable group rooted at \(v\), which is precisely the definition of an SCC.
Complexity Analysis
A single DFS traversal, with \(O(1)\) amortized work per stack push/pop:
$$\text{Time: } O(V + E) \qquad \text{Space: } O(V)$$
Implementation
Kosaraju's vs. Tarjan's
| Aspect | Kosaraju's | Tarjan's |
|---|---|---|
| DFS passes | 2 | 1 |
| Needs transpose graph? | yes | no |
| Extra data structure | finish-time list | low-link values + explicit stack |
| Conceptual simplicity | easier to explain and prove | more efficient, slightly subtler bookkeeping |
Both run in \(O(V+E)\); the practical choice usually comes down to whichever is easier to adapt into a larger codebase — Kosaraju's two clean DFS passes are often preferred for teaching and quick implementation, while Tarjan's single-pass version is preferred in performance-sensitive production code that can't afford to build and store a transpose graph.
Exercises
- Trace Tarjan's algorithm by hand on a 6-vertex graph with three SCCs, recording disc/low values at every step.
- Explain why a cross edge to a vertex not on the stack (already fully processed and popped as part of an earlier SCC) must be ignored in the low-link update — what would go wrong if it weren't?
- Modify the implementation to also print the condensation graph's edges (which SCCs point to which), by tracking which SCC each vertex belongs to after the algorithm finishes.
- Challenge: Convert the recursive implementation to a fully iterative one using an explicit call stack, and explain why this matters for very deep graphs (tying back to the recursion-depth discussion in the DFS deep dive).
Limitations
Trickier to Get Right Than It Looks
Tarjan's SCC algorithm has a well-earned reputation for being easy to implement subtly incorrectly — forgetting the "is \(w\) still on the stack" check when updating low-link values via a cross edge is the single most common bug, and it silently produces wrong SCCs rather than crashing. When correctness matters more than the last bit of performance, Kosaraju's simpler two-pass structure is often the safer choice to implement and verify.