← All Posts
DSA Series · Graphs · Strongly Connected Components

Kosaraju’s Algorithm: Finding Strongly Connected Components

In a directed graph, some groups of vertices are so tightly connected that you can reach every vertex in the group from every other vertex in the same group. These groups are called Strongly Connected Components (SCCs). Kosaraju’s Algorithm finds all of them in linear time using two elegant passes of DFS. In this post we will dissect the algorithm, walk through a visual example step by step, build an interactive animation, write production-quality C++ code, and prove why it works.

What Are Strongly Connected Components?

A Strongly Connected Component of a directed graph is a maximal set of vertices such that there is a directed path from every vertex in the set to every other vertex in the set. “Maximal” means you cannot add any more vertex without breaking this property.

Formal definition: A set S ⊆ V is an SCC if (1) for every pair u, v ∈ S there exist directed paths u →…→ v and v →…→ u, and (2) no proper superset of S also has this property.

Consider this directed graph with 8 nodes. It contains three SCCs, highlighted with dashed outlines:

SCC 1 SCC 2 SCC 3 0 1 2 3 4 5 6 7
SCC 1: {0, 1, 2} SCC 2: {3, 4} SCC 3: {5, 6, 7} Cross-SCC edge

Within each colored group, you can follow directed edges to get from any node to any other node in the same group. For instance, in the green group: 0→1→2→0 forms a cycle, so every pair is mutually reachable. But node 0 can reach node 3 (via 1→3), while node 3 cannot reach node 0 — so they belong to different SCCs.

Why Do SCCs Matter?

SCCs appear everywhere:

Any time you need to decompose a directed graph into its “tightly connected clusters,” you need an SCC algorithm.

The Set Intersection Approach (Naive)

Before studying Kosaraju’s clever algorithm, let’s understand the most natural way to compute SCCs — the set intersection approach. This builds the core intuition you need.

The Simple Idea

Think about what it means for two nodes to be in the same SCC: they can reach each other. So for any node v, its SCC is simply the set of all nodes that satisfy both conditions:

  1. v can reach u — there exists a directed path from v to u in G
  2. u can reach v — there exists a directed path from u to v in G

We can find each of these sets independently:

➡ Forward Reachability R(v)

“Where can I go from v?”

Run DFS/BFS starting at v on the original graph G. Every node you visit is reachable from v.

R(v) = { u : v can reach u }

⬅ Backward Reachability RT(v)

“Who can reach me?”

Run DFS/BFS starting at v on the transposed graph GT (all edges reversed). Every node you visit can reach v in the original graph.

RT(v) = { u : u can reach v }

The SCC of v is simply the intersection of these two sets:

Key identity: SCC(v) = R(v) ∩ RT(v)
A node u is in the same SCC as v if and only if v can reach u AND u can reach v.

Worked Example: Finding SCC(0)

Let’s trace this on our example graph for node 0:

Forward DFS from 0 on G (original graph)

Starting at node 0, we follow directed edges: 0→1→2→0 (back to start, skip), 1→3→4→5→6→7→5 (back, skip), 2→4 (already visited).

R(0) = {0, 1, 2, 3, 4, 5, 6, 7} — node 0 can reach everyone

Backward DFS from 0 on GT (reversed edges)

In GT, edge u→v becomes v→u. Starting at 0 on GT: 0→2 (reversed 2→0), 2→1 (reversed 1→2), 1→0 (reversed 0→1, already visited). No more unvisited neighbors.

RT(0) = {0, 1, 2} — only these three can reach node 0

Intersect the two sets

SCC(0) = R(0) ∩ RT(0) = {0,1,2,3,4,5,6,7} ∩ {0,1,2}

SCC(0) = {0, 1, 2} ✓

This matches! Nodes 3–7 are reachable from 0, but they cannot reach back to 0, so they’re in different SCCs.

Set Intersection — Step Through

Step through computing SCC(0): forward reachability (green), backward reachability (orange), then intersection (purple).

0 1 2 3 4 5 6 7
Phase
Ready
R(0) — Forward
{ }
RT(0) — Backward
{ }
SCC(0) = intersection
{ }
Click Step to begin.
Unvisited In R(0) In Rᵀ(0) In SCC Rejected

The Algorithm (Pseudocode)

To find all SCCs naively, repeat this process for each unassigned node:

function naive_sccs(V, adj):
adj_t = transpose(adj)
assigned = [false] * V
sccs = []
for v = 0 to V-1:
if not assigned[v]:
  forward = dfs(v, adj)    // R(v) — who can I reach?
  backward = dfs(v, adj_t) // RT(v) — who can reach me?
  scc = forward ∩ backward // both directions = same SCC
  sccs.append(scc)
  mark all in scc as assigned
return sccs

Why This Is Too Slow

For each unassigned node, we run two full DFS traversals. In the worst case (e.g., every node is its own SCC), that’s V nodes × O(V+E) per DFS = O(V × (V+E)) total. For V = 200,000 and E = 500,000, that’s ~140 billion operations — way too slow.

Approach Time DFS Passes Space
Naive Set IntersectionO(V × (V+E))2 per node = 2V totalO(V+E)
Kosaraju’s AlgorithmO(V+E)Exactly 2 totalO(V+E)

The genius of Kosaraju’s algorithm is that it computes all these intersections simultaneously using just two total DFS passes instead of two per node. The secret ingredient is the finish order from Pass 1, which cleverly batches the work. We’ll see exactly how the two are equivalent after understanding the algorithm.

Prerequisites

Before diving into Kosaraju’s algorithm, make sure you’re comfortable with these concepts:

DFS & Finish Times

Depth-First Search explores a graph by going as deep as possible before backtracking. A critical concept for Kosaraju’s algorithm is the finish time of each node — the moment DFS finishes processing a node (i.e., all its descendants have been fully explored and DFS backtracks from it).

Key insight: If you push each node onto a stack when it finishes (not when it’s first visited), the stack captures a topological-like ordering that is essential for the second pass of Kosaraju’s algorithm.

In our example, running DFS from node 0 produces this finish order (earliest to latest): 3, 7, 6, 5, 4, 2, 1, 0. Pushed onto a stack, the top is 0 (latest finish) and the bottom is 3 (earliest finish).

DFS Finish Order — Step Through

Step through DFS from node 0. When a node finishes (all descendants explored), it gets pushed onto the finish stack.

0 1 2 3 4 5 6 7
Finish Stack (top → bottom)
[ empty ]
DFS Call Stack
[ empty ]
Click Step to begin DFS from node 0.
Unvisited Current In Call Stack Finished

Graph Transpose

The transpose (or reverse) of a directed graph G is a new graph GT with the same vertices but every edge reversed. If G has edge u→v, then GT has edge v→u.

Key property: G and GT have exactly the same SCCs. Reversing all edges doesn’t break the mutual reachability within any SCC — if you could go from u to v and back in G, you can still do so in GT (the paths just swap directions).

Condensation Graph (DAG of SCCs)

If you collapse each SCC into a single “super-node,” the resulting graph is always a DAG (Directed Acyclic Graph). This is called the condensation of G. It captures the high-level structure of how SCCs connect to each other. We’ll explore this in detail later in the post.

Kosaraju’s Algorithm — The Idea

Kosaraju’s algorithm (also called the Kosaraju-Sharir algorithm) finds all SCCs using two passes of DFS. The key insight is beautifully simple:

Core intuition: The first DFS pass computes a finishing order that guarantees the second DFS pass (on the transposed graph) will discover one complete SCC per DFS tree.

Here’s why this works intuitively:

  1. Pass 1 discovers the “reachability frontier” of each node. Nodes that finish later in DFS can reach more of the graph. In particular, a node in an SCC that has outgoing cross-SCC edges will finish later than nodes in the “downstream” SCCs.
  2. Transposing the graph reverses the direction of cross-SCC edges. So if SCC A had an edge to SCC B in the original graph, now SCC B has an edge to SCC A in the transpose.
  3. Pass 2 processes nodes in reverse finish order (latest finish first). Starting DFS from the node with the latest finish time on the transposed graph, DFS can only reach nodes in the same SCC — because cross-SCC edges now point the wrong way (toward already-processed SCCs).

Think of it like this: Pass 1 determines the “pecking order” of SCCs, and Pass 2 uses that order to peel off one SCC at a time, starting from the “source” SCCs of the condensation DAG.

The Three Steps

1

DFS on G — Build Finish Stack

Run DFS on the original graph. As each node finishes, push it onto a stack.

2

Transpose G → GT

Reverse every edge in the graph.

3

DFS on GT in Stack Order

Pop nodes from the stack. For each unvisited node, run DFS on GT — all nodes visited form one SCC.

function kosaraju(V, adj):
stack = []
visited = [false] * V

// Pass 1: DFS on original graph, record finish order
for v = 0 to V-1:
if not visited[v]:
  dfs1(v, adj, visited, stack)

// Step 2: Build transposed graph
adj_t = transpose(adj)

// Pass 2: DFS on G^T in reverse finish order
visited = [false] * V
sccs = []
while stack is not empty:
v = stack.pop()
if not visited[v]:
  component = []
  dfs2(v, adj_t, visited, component)
  sccs.append(component)
return sccs

Kosaraju’s Algorithm — Step-by-Step Animation

Step through Kosaraju’s algorithm to see both DFS passes with color-coded SCCs.

0 1 2 3 4 5 6 7

C++ Implementation

Here is a clean, well-commented C++ implementation of Kosaraju’s algorithm:

#include <bits/stdc++.h>
using namespace std;

class KosarajuSCC {
    int V;
    vector<vector<int>> adj;    // original graph
    vector<vector<int>> adj_t;  // transposed graph

    // Pass 1: DFS on original graph, push to stack on finish
    void dfs1(int u, vector<bool>& visited, stack<int>& stk) {
        visited[u] = true;
        for (int v : adj[u]) {
            if (!visited[v])
                dfs1(v, visited, stk);
        }
        stk.push(u);  // push when node finishes
    }

    // Pass 2: DFS on transposed graph, collect component
    void dfs2(int u, vector<bool>& visited, vector<int>& component) {
        visited[u] = true;
        component.push_back(u);
        for (int v : adj_t[u]) {
            if (!visited[v])
                dfs2(v, visited, component);
        }
    }

public:
    KosarajuSCC(int V) : V(V), adj(V), adj_t(V) {}

    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj_t[v].push_back(u);  // build transpose simultaneously
    }

    vector<vector<int>> findSCCs() {
        // Step 1: Fill the stack with finish order
        vector<bool> visited(V, false);
        stack<int> stk;
        for (int i = 0; i < V; i++) {
            if (!visited[i])
                dfs1(i, visited, stk);
        }

        // Step 2: Process nodes in reverse finish order on G^T
        fill(visited.begin(), visited.end(), false);
        vector<vector<int>> sccs;

        while (!stk.empty()) {
            int u = stk.top();
            stk.pop();
            if (!visited[u]) {
                vector<int> component;
                dfs2(u, visited, component);
                sccs.push_back(component);
            }
        }
        return sccs;
    }
};

Usage: Create a KosarajuSCC(V) object, call addEdge(u,v) for each directed edge, then call findSCCs(). The transpose is built simultaneously inside addEdge()—no separate pass needed.

Complexity Analysis

Time Complexity: O(V + E)

Let’s break down each step:

Total: O(V + E) — linear in the size of the graph. This is optimal because you need to examine every vertex and edge at least once to determine SCCs.

Space Complexity: O(V + E)

Total: O(V + E).

Why Does It Work? (Correctness Proof Intuition)

The correctness of Kosaraju’s algorithm rests on a key lemma about finish times and the structure of the condensation DAG.

Key Lemma

Lemma: If there is an edge from SCC C to SCC C′ in the condensation DAG, then the maximum finish time in C is greater than the maximum finish time in C′.

Proof sketch: Consider an edge u→v where u∈C and v∈C′. During DFS Pass 1, there are two cases:

  1. DFS reaches C before C′: DFS from some node in C will eventually cross into C′ via u→v. All of C′ will finish before the DFS in C finishes (since we must backtrack through C after C′ is done). So the last node in C finishes after the last node in C′.
  2. DFS reaches C′ before C: Since C′ has no edge back to C (the condensation is a DAG), DFS on C′ cannot enter C. All of C′ finishes before C is even started. Again, max finish time of C > max finish time of C′.

Why Reverse Finish Order on GT Works

By the lemma, the SCC with the globally highest finish time is a source in the condensation DAG (no SCC has an edge pointing to it). When we start DFS from a node in this source SCC on GT:

After this SCC is identified and marked, the next highest-finish-time unvisited node belongs to the next source SCC (in the condensation DAG minus the already-found SCCs). The same argument applies recursively.

This is why the algorithm peels off SCCs one at a time in reverse topological order of the condensation DAG. Each DFS on GT is perfectly contained within exactly one SCC.

Equivalence to Set Intersection — Why Kosaraju’s Is Preferred

Recall the naive set intersection approach: SCC(v) = R(v) ∩ RT(v). Kosaraju’s algorithm computes exactly the same intersections, but does so for all nodes simultaneously in linear time. Let’s see why.

What Pass 2 Really Computes

In Pass 2, when we start a DFS from node v on GT, we find all nodes reachable from v in GT that haven’t been assigned to an SCC yet. This set is:

RT(v) ∩ {unassigned nodes} = {u : u can reach v in G, and u is not yet in any SCC}

The critical insight is that the finish order from Pass 1 guarantees this restricted set is exactly SCC(v). Here’s why:

The finish order makes restricted backward reachability = full intersection

When we process v in reverse finish order on GT:

  • Nodes in SCC(v) are all reachable from v in GT (since SCCs are the same in G and GT), and none have been assigned yet (by the finish-time lemma, SCC(v) is a source among remaining SCCs).
  • Nodes NOT in SCC(v) but reachable from v in GT must belong to SCCs that were sources in earlier iterations. By the finish-time lemma, those SCCs had higher max finish times, so they were processed and assigned before v.

Therefore, DFS from v on GT restricted to unassigned nodes visits exactly SCC(v) = R(v) ∩ RT(v).

Side-by-Side Comparison

Aspect Naive Intersection Kosaraju’s
Forward pass DFS from each v on G → R(v) Single DFS on G → finish order (encodes all R(v) implicitly)
Backward pass DFS from each v on GT → RT(v) Single DFS on GT in finish order → RT(v) restricted to unassigned
Intersection Explicit: R(v) ∩ RT(v) per node Implicit: finish order + visited array automatically restrict to SCC(v)
Time O(V × (V + E)) O(V + E)
Key trick None — brute force Finish order batches all intersections into one pass

Why Kosaraju’s Is Preferred Over Naive Intersection

  1. Linear time: Two DFS passes = O(V + E) total. The naive approach is O(V²) or worse.
  2. No explicit set operations: No hash sets, no intersection computation. The finish order and visited array handle everything implicitly.
  3. Simple implementation: Just two DFS passes and a stack. No complex data structures needed.
  4. Correctness is elegant: The equivalence to set intersection makes the algorithm easy to reason about and verify.
Takeaway: Kosaraju’s algorithm is not a fundamentally different approach from set intersection — it’s an optimized batched version. The finish order from Pass 1 encodes enough information about forward reachability that Pass 2’s restricted backward DFS automatically yields the correct intersection for each SCC. This is what makes it both correct and efficient.

The Condensation Graph

The condensation of a directed graph G is formed by collapsing each SCC into a single super-node and keeping only the cross-SCC edges (removing duplicates). The result is always a DAG.

For our example graph, the condensation looks like this:

SCC 1 {0, 1, 2} SCC 2 {3, 4} SCC 3 {5, 6, 7}

Notice the condensation is a simple chain: SCC 1 → SCC 2 → SCC 3. There are no cycles (by definition of a condensation). The cross-SCC edges 1→3, 2→4 both map to the single edge SCC 1→SCC 2, and edge 4→5 maps to SCC 2→SCC 3.

Why Is the Condensation Useful?

Practice Problems

Here are problems to solidify your understanding of SCCs and Kosaraju’s algorithm, ordered from foundational to advanced:

Problem Source Difficulty Key Idea
Flight Routes Check CSES Easy Check if entire graph is one SCC
Planets and Kingdoms CSES Easy Direct SCC decomposition — assign each node to its SCC
Coin Collector CSES Medium SCC condensation + DP on DAG
Checkposts CF 427C Medium Find SCCs, pick min-cost node per SCC, count choices
Catowice City CF 1239D Medium 2-SAT via SCC — implication graph analysis
Reachability from the Capital CF 999E Medium SCC condensation + count sources not reachable from capital
How Many Paths? CF 1547G Hard SCC condensation + reachability classification (0, 1, ∞)
Ralph and Mushrooms CF 894E Hard SCC condensation + max-path DP on condensation DAG

Summary

Let’s recap the key takeaways:

  1. Strongly Connected Components are maximal sets of vertices with mutual reachability in directed graphs.
  2. The set intersection approach (SCC(v) = R(v) ∩ RT(v)) is the natural definition but costs O(V × (V+E)).
  3. Kosaraju’s Algorithm is an optimized batched version of set intersection that finds all SCCs in O(V + E) using two DFS passes:
    • Pass 1: DFS on G, build finish-order stack
    • Transpose G to get GT
    • Pass 2: DFS on GT in reverse finish order
  4. The algorithm works because the finish order encodes forward reachability implicitly, and processing in reverse finish order on GT confines each DFS to exactly one SCC — computing the intersection without explicit set operations.
  5. The condensation graph (DAG of SCCs) captures the macro-structure and is useful for reachability queries, 2-SAT, and more.
  6. Kosaraju’s runs in O(V + E) time and O(V + E) space (storing both G and GT).
Practice tip: Implement Kosaraju’s from scratch at least 3 times before a contest. The two-DFS pattern becomes second nature with practice. Focus on understanding why finish order matters — that insight transfers to many other graph problems.