Coding Interview Patterns Module 23 – Weighted graphs: shortest paths and spanning trees
Strongly connected components: Kosaraju and Tarjan
Districts of one-way streets in which every junction reaches every other: strongly connected components, with the brute force and the signal.
What you will learn
- Find strongly connected components with Kosaraju's two passes or Tarjan's single pass, both in O(n + m)
- Prove that Kosaraju's second pass never leaks from one component into another
- Build the condensation and use its sources and sinks to count the fewest new edges for strong connectivity
- Solve reachability and path questions with dynamic programming over the condensation, including 2-SAT
- Write both searches with stacks of their own, so that a chain of 100,000 nodes cannot overflow the call stack
Before you start
What you’ll learn · Before you start
What you will learn
- Find strongly connected components with Kosaraju's two passes or Tarjan's single pass, both in O(n + m)
- Prove that Kosaraju's second pass never leaks from one component into another
- Build the condensation and use its sources and sinks to count the fewest new edges for strong connectivity
- Solve reachability and path questions with dynamic programming over the condensation, including 2-SAT
- Write both searches with stacks of their own, so that a chain of 100,000 nodes cannot overflow the call stack
Before you start
On this page
The strongly connected components of a directed graph are its largest groups of nodes that can all reach one another along the edge directions. Two linear-time methods find them. Kosaraju’s algorithm runs two depth-first searches, the second over the reversed edges in reverse finishing order. Tarjan’s algorithm runs a single depth-first search with a stack and low-links. Shrinking each component to one node leaves a graph without cycles, the condensation.
The problem: districts of one-way streets
A city has n junctions, numbered 0 to n − 1, and m one-way streets; street (a, b) leads from junction a
to a different junction b, and a street may be listed twice. A district is a largest group of junctions in which
every junction can drive to every other, following streets the way they point. (A) How many districts are there, and
which district holds each junction? (B) The city may open new one-way streets: what is the fewest number that lets
every junction reach every other? Limits: 1 ≤ n ≤ 100,000 and 0 ≤ m ≤ 200,000.
- Nine junctions with streets 0→1, 1→2, 2→0, 2→3, 3→4, 4→5, 5→3, 5→6, 6→7, 7→6 and 8→4 form four districts,
{0, 1, 2},{3, 4, 5},{6, 7}and{8}, and need 2 new streets. - One junction is one district and needs none.
- Two junctions with the street 0→1 are two districts and need 1, the street 1→0.
The first example and its four districts
Text description of the diagram
Nine numbered junctions with one-way streets drawn as arrows, and four dashed boxes around the districts. District A holds junctions 0, 1 and 2, joined in a loop: 0 to 1, 1 to 2 and 2 to 0. An arrow leaves district A from junction 2 to junction 3.
District B holds junctions 3, 4 and 5, joined in a second loop: 3 to 4, 4 to 5 and 5 to 3. An arrow leaves district B from junction 5 to junction 6. District C holds junctions 6 and 7, with one street each way between them. District D, at the bottom, holds junction 8 alone, with a single street from 8 into junction 4 of district B. Inside each box every junction can reach every other; between boxes, streets lead only from A and D into B, and from B into C. A legend explains the arrows and the dashed boxes.
The brute force, and why it fails at 100,000 junctions
Search from every junction, and put two junctions in one district when each reaches the other. For part B, try every
set of k new streets, for k = 1, 2 and so on, until the city is one district:
"""The brute force for districts of one-way streets: a search from every junction, then a pair of junctions shares a
district when each reaches the other. For the fewest new streets, try every set of k new streets for k = 1, 2, ..."""
from itertools import combinations
def reach_from(n, near, s):
seen, todo = {s}, [s]
while todo:
for v in near[todo.pop()]:
if v not in seen:
seen.add(v)
todo.append(v)
return seen
def districts(n, streets):
near = [[] for _ in range(n)]
for a, b in streets:
near[a].append(b)
reach = [reach_from(n, near, s) for s in range(n)] # n searches, n * n answers kept
label = [-1] * n
count = 0
for u in range(n):
if label[u] == -1:
for v in range(n):
if v in reach[u] and u in reach[v]:
label[v] = count
count += 1
return count, label
def fewest_new_streets(n, streets):
if districts(n, streets)[0] == 1:
return 0, 0
candidates = [(a, b) for a in range(n) for b in range(n) if a != b]
tried = 0
for k in range(1, n + 1):
for extra in combinations(candidates, k):
tried += 1
if districts(n, streets + list(extra))[0] == 1:
return k, tried
NINE = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3), (5, 6), (6, 7), (7, 6), (8, 4)]
for name, n, streets in [("nine junctions", 9, NINE), ("one junction", 1, []), ("two junctions", 2, [(0, 1)])]:
count, label = districts(n, streets)
k, tried = fewest_new_streets(n, streets)
found = f"fewest new streets {k}, found after trying {tried:,} sets" if k else "no new street needed"
print(f"{name}: {count} district{'s' * (count != 1)} {label}; {found}")
n, m = 100_000, 200_000
print(f"{n:,} junctions: {n:,} searches of up to {n + m:,} steps, {n * (n + m):,} in all, and {n * n:,} reach answers")
print(f"new streets at {n:,} junctions: {n * (n - 1):,} candidates for a single street alone") Output
nine junctions: 4 districts [0, 0, 0, 1, 1, 1, 2, 2, 3]; fewest new streets 2, found after trying 589 sets one junction: 1 district [0]; no new street needed two junctions: 2 districts [0, 1]; fewest new streets 1, found after trying 2 sets 100,000 junctions: 100,000 searches of up to 300,000 steps, 30,000,000,000 in all, and 10,000,000,000 reach answers new streets at 100,000 junctions: 9,999,900,000 candidates for a single street alone
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.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
It finds four districts and 2 new streets after trying 589 sets. At the limits, though, that is 100,000 searches of up to 300,000 steps, 3 × 10¹⁰ in all, and 10¹⁰ reach answers to keep, about 1.25 GB even at one bit each. Part B is worse: a single new street already has 9,999,900,000 candidates.
What to notice
- Reaching each other is an equivalence. If
uandvreach each other, and so dovandw, thenuandwdo too, so the districts split the junctions into groups that never overlap. - Between districts, streets go one way. Streets leading from district X to Y and also back would make X and Y one district, so the graph of districts has no cycle.
- Part B lives on that graph. It depends on the sources, districts that no street enters, and the sinks, districts that no street leaves.
Three methods follow:
- Kosaraju’s two passes record the order in which a search finishes the junctions, then search the reversed streets, latest finisher first, collecting one district per start.
- Tarjan’s single pass keeps the current search’s junctions on a stack and closes a district when a junction’s low-link equals its own number.
- The condensation has one node per district and an arrow wherever a street joins two districts.
Kosaraju’s two passes, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the second pass never leaks into another district, proved
- Tarjan’s single pass with low-links
- A dry run on the nine junctions
- The condensation and the fewest new streets, proved
- Both searches with stacks of their own
- The code in five languages
- Kosaraju or Tarjan: the trade-offs
- How much work it does
- Edge cases and common bugs
- Variants: the longest drive and 2-SAT
- Follow-up questions
- Exercise: districts and sources
- Count the districts and the districts nothing leads into
- A practice set with sample answers
- Check yourself
- Interview questions
- Why does the graph of strongly connected components, the condensation, never contain a cycle?
- A platform has 3,000 services and 40,000 dependencies of the form "A calls B". Releases must deploy a service after everything it calls, but some services call each other in cycles. How do you find the groups that must ship together and an order for the groups?
- An airline's 600 airports are joined by one-way routes, and some airports cannot be reached from others. What is the fewest number of new one-way routes that lets every airport reach every other, and why can fewer not work?
- A release has 200 feature flags, each on or off, and 1,500 rules such as "if A is on, B must be off" or "A or B must be on". Before each release you must say whether some setting satisfies every rule and produce one. How, in linear time?
- Key takeaways
1 diagram · 5 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 26 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; nothing renews unless you choose to. 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).
References
- Depth-First Search and Linear Graph Algorithms (R. E. Tarjan, SIAM Journal on Computing 1(2), pages 146–160) (Society for Industrial and Applied Mathematics (SIAM))
- A strong-connectivity algorithm and its applications in data flow analysis (M. Sharir, Computers & Mathematics with Applications 7(1), pages 67–72) (Elsevier)
- Augmentation Problems (K. P. Eswaran and R. E. Tarjan, SIAM Journal on Computing 5(4), pages 653–665) (Society for Industrial and Applied Mathematics (SIAM))
- A linear-time algorithm for testing the truth of certain quantified boolean formulas (B. Aspvall, M. F. Plass and R. E. Tarjan, Information Processing Letters 8(3), pages 121–123) (Elsevier)
- strongly connected component (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- sys.getrecursionlimit() and sys.setrecursionlimit() (The Python Standard Library) (Python Software Foundation)
- TypedArray (MDN Web Docs)
- Class StackOverflowError (Java SE 25 API) (Oracle)
- AddressSanitizer (Clang documentation) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress