Data Structures & Algorithms (DSA) Module 11 – Graphs: modelling, traversal and connectivity
Strongly connected components
Find the groups of a directed graph in which every vertex reaches every other, with two depth-first passes, and shrink them into a graph without cycles.
What you will learn
- Find SCCs with Kosaraju's two-pass algorithm
- Find SCCs with Tarjan's low-link algorithm
- Build and use the condensation DAG
Before you start
What you’ll learn · Before you start
What you will learn
- Find SCCs with Kosaraju's two-pass algorithm
- Find SCCs with Tarjan's low-link algorithm
- Build and use the condensation DAG
Before you start
On this page
A strongly connected component (SCC) of a directed graph is a largest set of vertices in which every vertex can reach every other one by following edges in their direction. Every vertex lies in exactly one component, so the components split the vertices into disjoint groups; a vertex on no cycle is a component of its own. Shrink each component to a single vertex and the result, the condensation, has no cycles. It is a DAG, so a question about a tangled directed graph becomes a question about a DAG, where topological order applies. Two classic algorithms find every component in O(V + E) time. Kosaraju’s algorithm runs depth-first search twice, the second time over the reversed edges. Tarjan’s algorithm needs only one depth-first search, plus a stack and one more number for every vertex.
A link graph and its components
Ten pages of a site link to one another. Home, blog and archive link in a loop, so each of them reaches the other two; docs and api link to each other; shop, cart and checkout form a second loop; status and receipt sit on no loop at all.
Ten pages, five strongly connected components
Text description of the diagram
The figure shows ten pages and their links, with every strongly connected component drawn as a box.
- Component 0 holds home, blog and archive, which link in a loop: home to blog, blog to archive, archive to home.
- Component 1 holds docs and api, which link to each other.
- Component 3 holds shop, cart and checkout, in a loop: shop to cart, cart to checkout, checkout to shop.
- Component 2 holds status alone, and component 4 holds receipt alone.
The links between boxes all point one way: archive to docs, blog to shop, api to shop, docs to status, checkout to receipt and status to receipt. No link leads back from a lower box to a higher one, so the boxes themselves form a graph without cycles, the condensation. Its links are 0 to 1, 0 to 3, 1 to 2, 1 to 3, 2 to 4 and 3 to 4.
Archive links to docs, yet docs is not in the component of archive: nothing that leaves docs ever leads back to home, blog or archive. Reaching in one direction is not enough. The links between the boxes all point one way, from component 0 towards component 4.
Why the condensation has no cycles
Suppose component A could reach component B and B could reach A. Then every vertex of A would reach every vertex of B and back, so A and B together would be one larger set of mutually reachable vertices, and neither could be a largest one. So the links between components can never close a cycle, and the condensation is always a DAG. Every DAG has at least one source, a component that no other component reaches, and at least one sink, a component that reaches no other.
Kosaraju’s two passes, step by step
- Run depth-first search over the whole graph and record each vertex at the moment it finishes, when all its edges have been explored.
- Reverse every edge.
- Take the vertices in reverse finishing order. From each vertex not yet in a component, search the reversed graph; every vertex this search reaches that has no component yet belongs to the new component.
The vertex that finishes last always lies in a source component of the condensation. Reversing the edges turns that source into a sink, so a search from it in the reversed graph cannot leave its own component. The program below writes both passes with explicit stacks, so it never recurses:
# Kosaraju's algorithm on a small link graph of ten pages, with both passes written as loops (no recursion).
# Pass 1: depth-first search on the graph, recording each page when it finishes.
# Pass 2: depth-first search on the reversed graph, starting from pages in reverse finishing order; every search of
# this pass collects exactly one strongly connected component.
PAGES = ["home", "blog", "archive", "docs", "api", "shop", "cart", "checkout", "receipt", "status"]
LINKS = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 3), (4, 5), (1, 5), (5, 6), (6, 7), (7, 5), (7, 8), (3, 9), (9, 8)]
def finishing_order(n, adj):
seen, order = [False] * n, []
for s in range(n):
if seen[s]:
continue
seen[s] = True
stack = [(s, 0)] # (page, index of the next link to try)
while stack:
v, i = stack.pop()
if i < len(adj[v]):
stack.append((v, i + 1))
w = adj[v][i]
if not seen[w]:
seen[w] = True
stack.append((w, 0))
else:
order.append(v) # every link of v is explored: v finishes
return order
def kosaraju(n, links):
adj, radj = [[] for _ in range(n)], [[] for _ in range(n)]
for u, v in links:
adj[u].append(v)
radj[v].append(u) # the reversed graph
order = finishing_order(n, adj)
comp, components = [-1] * n, []
for s in reversed(order):
if comp[s] >= 0:
continue
comp[s], members, stack = len(components), [s], [s]
while stack:
v = stack.pop()
for w in radj[v]:
if comp[w] < 0:
comp[w] = len(components)
members.append(w)
stack.append(w)
components.append(sorted(members))
return order, comp, components
order, comp, components = kosaraju(len(PAGES), LINKS)
print("pass 1, pages as they finish:", " ".join(PAGES[v] for v in order))
for i, members in enumerate(components):
print(f"pass 2, component {i}: {', '.join(PAGES[v] for v in members)}")
dag = sorted({(comp[u], comp[v]) for u, v in LINKS if comp[u] != comp[v]})
print("links between components:", ", ".join(f"{a} -> {b}" for a, b in dag)) Output
pass 1, pages as they finish: receipt checkout cart shop api status docs archive blog home pass 2, component 0: home, blog, archive pass 2, component 1: docs, api pass 2, component 2: status pass 2, component 3: shop, cart, checkout pass 2, component 4: receipt links between components: 0 -> 1, 0 -> 3, 1 -> 2, 1 -> 3, 2 -> 4, 3 -> 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kosaraju.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Receipt finishes first, since it links nowhere, and home finishes last. The second pass starts from home, collects home, blog and archive in the reversed graph and stops there, because no page outside that group links into it, so the reversed links offer no way out. The components come out in topological order of the condensation: every link between components runs from a lower number to a higher one.
Tarjan’s algorithm finds the same components in one search. It numbers the vertices as it discovers them, keeps for each the smallest number its subtree can reach through vertices still on a stack, and closes a component when a vertex can reach nothing older than itself.
| Operation | Time | Extra space |
|---|---|---|
| Kosaraju: two depth-first passes | O(V + E) | O(V + E): the reversed graph |
| Tarjan: one depth-first pass | O(V + E) | O(V): indexes, low-links, a stack |
| Condensation from the component ids | O(V + E) | O(V + E) |
Exercise and self-check
Count the strongly connected components of a graph and the size of the largest, on graphs up to a cycle of 100,000 vertices.
Exercise · Hard · Python, JavaScript
Count the strongly connected components and the largest one
A directed graph has n vertices, numbered from 0, and a list of directed edges (u, v) (JavaScript: two-element arrays). Write scc_sizes(n, edges) (JavaScript: sccSizes) that returns two numbers: how many strongly connected components the graph has, and how many vertices the largest one holds. Python returns a tuple (count, largest), JavaScript an array [count, largest].
A vertex with no edges is a component of its own, and so is a vertex whose only edge is a loop to itself. Edges may repeat. The sample tests check the ten-page graph of the lesson, the small cases, random graphs against a slow reachability check, and a single cycle of 100,000 vertices, which needs a search that does not recurse once per vertex.
Python · Starter code · scc.py
def scc_sizes(n, edges):
"""(number of strongly connected components, number of vertices in the largest one)."""
raise NotImplementedError("write scc_sizes") The sample tests · test_scc.py
import random
from scc import scc_sizes
PAGES = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 3), (4, 5), (1, 5), (5, 6), (6, 7), (7, 5), (7, 8), (3, 9), (9, 8)]
def slow_sizes(n, edges):
"""Reachability from every vertex, then mutual reachability: right, and O(V (V + E))."""
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
reach = []
for s in range(n):
seen, stack = {s}, [s]
while stack:
for w in adj[stack.pop()]:
if w not in seen:
seen.add(w)
stack.append(w)
reach.append(seen)
groups = {frozenset(v for v in reach[s] if s in reach[v]) for s in range(n)}
return len(groups), max((len(g) for g in groups), default=0)
def test_lesson_graph():
"""the ten pages of the lesson: five components, the largest of three pages"""
assert scc_sizes(10, PAGES) == (5, 3)
def test_small_cases():
"""one vertex, a loop, a repeated edge, a chain and a two-way link"""
assert scc_sizes(1, []) == (1, 1)
assert scc_sizes(1, [(0, 0)]) == (1, 1)
assert scc_sizes(2, [(0, 1), (0, 1)]) == (2, 1)
assert scc_sizes(5, [(0, 1), (1, 2), (2, 3), (3, 4)]) == (5, 1)
assert scc_sizes(3, [(0, 1), (1, 0)]) == (2, 2)
def test_random_against_reachability():
"""200 random graphs of up to 12 vertices, against a slow mutual-reachability check"""
rng = random.Random(21)
for _ in range(200):
n = rng.randint(1, 12)
edges = [(rng.randrange(n), rng.randrange(n)) for _ in range(rng.randint(0, 2 * n))]
assert scc_sizes(n, edges) == slow_sizes(n, edges), (n, edges)
def test_long_cycle_and_long_path():
"""a single cycle of 100,000 vertices, and a path of 100,000 vertices"""
n = 100_000
assert scc_sizes(n, [(i, (i + 1) % n) for i in range(n)]) == (1, n)
assert scc_sizes(n, [(i, i + 1) for i in range(n - 1)]) == (n, 1) JavaScript · Starter code · scc.mjs
/** [number of strongly connected components, number of vertices in the largest one]. */
export function sccSizes(n, edges) {
throw new Error('write sccSizes');
} The sample tests · scc.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { sccSizes } from './scc.mjs';
const PAGES = [[0, 1], [1, 2], [2, 0], [2, 3], [3, 4], [4, 3], [4, 5], [1, 5], [5, 6], [6, 7], [7, 5], [7, 8], [3, 9], [9, 8]];
/** Reachability from every vertex, then mutual reachability: right, and O(V (V + E)). */
function slowSizes(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) adj[u].push(v);
const reach = [];
for (let s = 0; s < n; s++) {
const seen = new Set([s]);
const stack = [s];
while (stack.length) {
for (const w of adj[stack.pop()]) {
if (!seen.has(w)) {
seen.add(w);
stack.push(w);
}
}
}
reach.push(seen);
}
const groups = new Set();
let largest = 0;
for (let s = 0; s < n; s++) {
const members = [...reach[s]].filter((v) => reach[v].has(s)).sort((a, b) => a - b);
groups.add(members.join(','));
largest = Math.max(largest, members.length);
}
return [groups.size, largest];
}
test('the ten pages of the lesson: five components, the largest of three pages', () => {
assert.deepEqual(sccSizes(10, PAGES), [5, 3]);
});
test('one vertex, a loop, a repeated edge, a chain and a two-way link', () => {
assert.deepEqual(sccSizes(1, []), [1, 1]);
assert.deepEqual(sccSizes(1, [[0, 0]]), [1, 1]);
assert.deepEqual(sccSizes(2, [[0, 1], [0, 1]]), [2, 1]);
assert.deepEqual(sccSizes(5, [[0, 1], [1, 2], [2, 3], [3, 4]]), [5, 1]);
assert.deepEqual(sccSizes(3, [[0, 1], [1, 0]]), [2, 2]);
});
test('200 random graphs of up to 12 vertices, against a slow mutual-reachability check', () => {
let seed = 21;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const pick = (k) => Math.floor(random() * k);
for (let t = 0; t < 200; t++) {
const n = 1 + pick(12);
const edges = Array.from({ length: pick(2 * n + 1) }, () => [pick(n), pick(n)]);
assert.deepEqual(sccSizes(n, edges), slowSizes(n, edges), JSON.stringify([n, edges]));
}
});
test('a single cycle of 100,000 vertices, and a path of 100,000 vertices', () => {
const n = 100000;
const cycle = [];
const path = [];
for (let i = 0; i < n; i++) cycle.push([i, (i + 1) % n]);
for (let i = 0; i < n - 1; i++) path.push([i, i + 1]);
assert.deepEqual(sccSizes(n, cycle), [1, n]);
assert.deepEqual(sccSizes(n, path), [n, 1]);
}); A hint
Kosaraju's two passes are the easier ones to write without recursion. In the first pass, keep a stack of (vertex, position of the next edge) pairs and append a vertex to the finishing order when its edges run out. In the second pass, search the reversed graph from the vertices in reverse finishing order; every search that starts there collects one component, and any order of visiting works inside it.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Why the second pass never leaks, proved, and Tarjan's low-links traced Pro: opens with a Pro, Premium or Ultimate pass
Kosaraju’s algorithm is short because one fact about finishing times does all the work. This part proves that fact, traces Tarjan’s single pass on the same ten pages and compares the two algorithms.
In this part:
- The finishing-time lemma, and the proof of both passes
- Tarjan’s low-links, traced on the ten pages
- Kosaraju or Tarjan: the trade-offs
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
A cycle of 200,000 pages, Tarjan in five languages and four bugs reproduced Pro: opens with a Pro, Premium or Ultimate pass
On a single cycle of pages, a depth-first search keeps every page open at the same time, so a recursive version nests one call per page. This part reproduces that crash, runs Tarjan’s algorithm with an explicit stack in five languages, and reproduces four bugs on small graphs.
In this part:
- Recursion on a long cycle, reproduced
- Tarjan without recursion in five languages, with run records
- Four bugs, each on a small graph
- Edge cases worth a test
4 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Working on the condensation: seed pages, the longest walk, new links and a practice set Pro: opens with a Pro, Premium or Ultimate pass
Once the components are known, the condensation turns hard questions about a directed graph into easy ones on a DAG. This part computes three of them on the ten pages and on 100,000 random pages, and ends with a practice set.
In this part:
- Seeds for a crawler and the longest walk, computed on the DAG
- The fewest new links that make a graph strongly connected
- A practice set, with worked answers
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why is the condensation of a directed graph, one vertex per strongly connected component, always acyclic?
Because a cycle among components would merge them. If component A reached component B and B reached A, then any vertex of A could reach any vertex of B through that path, and get back the other way, so every vertex of A and B would reach every other. A and B together would then be a larger set of mutually reachable vertices, but a component is by definition the largest such set. So the links between components only ever point one way, the condensation is a DAG, and it always has a topological order, at least one source and at least one sink. That is what lets a question about a tangled graph be answered with DAG methods on its components.
-
Mid to senior
A financial model has 40,000 formula cells and 300,000 references, and its engine refuses to calculate while any circular reference exists. Users need every group of cells caught in a circle, and an estimate of the work to untangle each. What do you compute, how long does it take, and how do you rank the groups?
-
Mid to senior
A link-analysis job finds strongly connected components with a recursive Tarjan in Python. It works on every test graph but crashes on the production link graph of 3 million pages. What is going on, and what are your options?
-
Mid to staff
A crawler must reach every page of a site whose link graph has 2 million pages, starting from as few seed pages as possible. How many seeds do you need, how do you choose them, and what changes when new pages and links appear every hour?
Key takeaways
- A strongly connected component is a largest set of vertices that all reach one another; together they split the vertices.
- The condensation, one vertex per component, never has a cycle, so DAG methods apply to it.
- Kosaraju’s algorithm searches twice, the second time over reversed edges in reverse finishing order.
- Tarjan’s algorithm finds the same components in one search with a stack and a low-link per vertex.
References
- Depth-First Search and Linear Graph Algorithms (R. E. Tarjan, SIAM Journal on Computing 1(2), 1972) (Society for Industrial and Applied Mathematics)
- A strong-connectivity algorithm and its applications in data flow analysis (M. Sharir, 1981) (Elsevier)
- Augmentation Problems (K. P. Eswaran and R. E. Tarjan, SIAM Journal on Computing 5(4), 1976) (Society for Industrial and Applied Mathematics)
- A linear-time algorithm for testing the truth of certain quantified boolean formulas (Aspvall, Plass and Tarjan, 1979) (Elsevier)
- Dictionary of Algorithms and Data Structures: strongly connected component (National Institute of Standards and Technology (NIST))
- sys.getrecursionlimit and sys.setrecursionlimit (Python Software Foundation)
- ECMAScript language specification: TypedArray objects (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress