Purpose: Find all Strongly Connected Components (SCCs) in a directed graph, in O(V + E) time using two passes of DFS.
Algorithm
- Run a DFS on the original graph, and whenever a vertex finishes (all its descendants are processed), push it onto a stack.
- Reverse every edge in the graph to build the transpose graph.
- Pop vertices off the stack one by one (highest finish time first). For each unvisited vertex popped, run a DFS on the transpose graph from it.
- Every vertex reached in this DFS belongs to the same SCC as the starting vertex.
- Repeat until the stack is empty. Each DFS tree from step 3 is exactly one SCC.
Code
void dfs1(int u, vector<vector<int>>& adj, vector<bool>& visited, stack<int>& order) {
visited[u] = true;
for (int v : adj[u]) if (!visited[v]) dfs1(v, adj, visited, order);
order.push(u);
}
void dfs2(int u, vector<vector<int>>& radj, vector<bool>& visited, vector<int>& component) {
visited[u] = true;
component.push_back(u);
for (int v : radj[u]) if (!visited[v]) dfs2(v, radj, visited, component);
}
vector<vector<int>> kosaraju(int n, vector<vector<int>>& adj) {
vector<bool> visited(n, false);
stack<int> order;
for (int i = 0; i < n; i++) if (!visited[i]) dfs1(i, adj, visited, order);
vector<vector<int>> radj(n);
for (int u = 0; u < n; u++)
for (int v : adj[u]) radj[v].push_back(u);
fill(visited.begin(), visited.end(), false);
vector<vector<int>> sccs;
while (!order.empty()) {
int u = order.top(); order.pop();
if (!visited[u]) {
vector<int> component;
dfs2(u, radj, visited, component);
sccs.push_back(component);
}
}
return sccs;
}Paradigm
Transform and Conquer. The core trick is instance simplification: reordering vertices by DFS finish time and transforming the graph into its transpose reduces the hard problem of finding SCCs directly into two straightforward DFS traversals.
Complexity
- Time: O(V + E)
- Space: O(V + E)
Proof of Correctness
Key Lemma: If there is an edge from SCC A to SCC B (A ≠ B) in the original graph, then the maximum finish time (from the first DFS) among vertices in A is greater than the maximum finish time among vertices in B.
Proof of lemma: Consider whichever of A or B the first DFS discovers first.
- If
Ais discovered first: since an edge leads fromAintoB, the DFS exploringAwill descend intoBbefore backtracking (no edge exists fromBback toA, or they’d be one SCC), so all ofBfinishes before the entry vertex ofAfinishes — givingAthe higher max finish time. - If
Bis discovered first: since there’s no edge fromBtoA, DFS starting inBcannot reachA, so all ofBfinishes completely before any vertex ofAis even discovered — again givingAthe higher max finish time.
Main proof: In the second pass, DFS runs on the transpose graph in decreasing order of finish time. Let u be the first unvisited vertex processed, belonging to SCC C, which has the highest remaining finish time. By the lemma (applied to the original graph), no other unvisited SCC D can have an edge into C in the original graph — otherwise D would need a higher finish time than C, contradicting C’s selection. This means no edge from C to an unvisited SCC exists in the transpose graph either. So the DFS from u on the transpose graph cannot escape C, and since C is strongly connected, it reaches every vertex in C — no more, no less. Repeating this argument for each subsequent pop correctly peels off one SCC at a time. ∎
Variants / Use Cases
- Tarjan’s Algorithm → alternative single-pass DFS approach using low-link values instead of two full passes
- Condensation graph construction → contract each SCC into a single node to get a DAG, useful for further DAG-based processing
- 2-SAT solving → build an implication graph and use SCCs to check satisfiability and extract a valid assignment
- Dead code / unreachable state elimination → SCCs identify cyclic dependency groups in compilers and state machines
- Web page/community detection → SCCs in web graphs or social graphs reveal mutually-reachable clusters