A strongly connected component (SCC) of a directed graph is a maximal set of vertices where every vertex can reach every other. Tarjan's algorithm finds all SCCs in a single depth-first search — a classic that shows up whenever a problem asks "which nodes are mutually reachable?"
Problem. Given a directed graph, partition its vertices into strongly connected components: groups
where, for any two vertices u and v in the same group, there is a path u → v and a path v → u.
Example: edges 0 → 1, 1 → 2, 2 → 0, 2 → 3, 3 → 4. The cycle ties 0, 1, 2 together, while 3
and 4 each stand alone → SCCs [0, 1, 2], [3], [4].
The slow way first
The brute-force definition is literal: for every pair of vertices, run a reachability check both ways and group the ones that reach each other. Each reachability test is a full traversal, so this is O(V³) or worse — hopeless for a large graph.
The question to ask: while I am exploring with DFS, can I detect a whole component the moment I finish exploring it? Tarjan answers yes, with two small integers per vertex.
The idea: disc and low
Run one DFS. As we first reach a vertex, stamp it with a discovery time disc (a counter that only goes up) and a low-link low, initially equal to disc. low[u] tracks the smallest disc reachable from u using tree edges and at most one back edge to a vertex still on the DFS stack.
We also keep a stack of vertices currently being explored. When a vertex u finishes with low[u] == disc[u], it is the root of an SCC — everything pushed on top of it on the stack belongs to that component, so we pop down to u.
The trick is how low is relaxed: a tree edge u → v pulls low[u] down to low[v], and a back edge to a vertex still on the stack pulls low[u] down to that vertex's disc. A cycle therefore drags every vertex on it down to one shared low value — the root.
Walk through it
Step through the animation. DFS dives 0 → 1 → 2. At 2 the edge 2 → 0 is a back edge to a stacked vertex, so low[2] drops to 0. DFS then explores 2 → 3 → 4. 4 and 3 each finish with low == disc and pop as singletons. Back in the cycle, low propagates up to 0, which finishes as a root and pops 2, 1, 0 together.
Pseudocode
t = 0; stack = []; sccs = []
dfs(u):
disc[u] = low[u] = t; t += 1
push u on stack; mark u on_stack
for each edge u -> v:
if v unvisited:
dfs(v); low[u] = min(low[u], low[v])
else if v on_stack:
low[u] = min(low[u], disc[v])
if low[u] == disc[u]: # u is an SCC root
pop vertices off stack down to u, collect as one SCC
for each vertex u:
if u unvisited: dfs(u)The Python solution
def tarjan(graph):
disc, low, on_stack = {}, {}, set()
stack, sccs, t = [], [], 0
def dfs(u):
nonlocal t
disc[u] = low[u] = t; t += 1
stack.append(u); on_stack.add(u)
for v in graph[u]:
if v not in disc:
dfs(v)
low[u] = min(low[u], low[v])
elif v in on_stack:
low[u] = min(low[u], disc[v])
if low[u] == disc[u]:
comp = []
while True:
w = stack.pop(); on_stack.discard(w)
comp.append(w)
if w == u: break
sccs.append(comp)
for u in graph:
if u not in disc: dfs(u)
return sccsdiscrecords when each vertex was first seen;lowis its low-link;on_stackis a fast membership set.- On entry we stamp
disc[u] = low[u] = tand pushuonto the stack. - A tree edge (
v not in disc) recurses, then relaxeslow[u]with the child'slow[v]. - A back edge to a vertex still
on_stackrelaxeslow[u]withdisc[v]— neverlow[v], which would be wrong for cross edges. - When
low[u] == disc[u],uis a root: pop the stack down to and includinguto collect exactly one SCC.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (pairwise reachability) | O(V³) (moderate) | a traversal per pair |
| Tarjan (this solution) | O(V + E) (moderate) | each vertex and edge visited once |
O(V) (moderate)A single DFS touches every vertex and edge exactly once, so the work is linear in the graph size. The extra space is the recursion stack plus the explicit vertex stack and the disc/low maps — all O(V).
When this pattern shows up
Reach for Tarjan (or Kosaraju) whenever you need mutually reachable groups: condensing a graph into a DAG of components, detecting cycles in directed graphs, 2-SAT solving, or finding strongly connected modules. The disc/low idea is the same machinery behind finding bridges and articulation points.
The most common bug is relaxing with low[v] on a back edge instead of disc[v]. Only use disc[v] for a
vertex still on the stack — and check on_stack, because an already-popped vertex belongs to a finished SCC
and must be ignored.
Practice
During DFS at vertex 2, the edge 2 → 0 is examined. Vertex 0 is already visited and still on the stack. What happens to low[2]?
1. What does low[u] represent?
2. When is a vertex u the root of an SCC?
3. On a back edge u → v where v is still on the stack, low[u] is relaxed with which value?
4. What is the time complexity of Tarjan's algorithm?