Algorithm tutorials
Strongly Connected Components (Tarjan and Kosaraju)
Split a directed graph into maximal groups of mutually reachable vertices in O(n + m), with iterative Tarjan or Kosaraju, then shrink each group to a vertex to get a DAG you can reason about.
This is an extension topic beyond most interviews; it pays off in hard OAs about directed graphs with cycles, where collapsing cycles into single vertices turns the problem into a DAG problem.
Intuition
In a directed graph, two vertices are strongly connected if each can reach the other. That relation splits the vertices into strongly connected components (SCCs): maximal groups where everybody reaches everybody. A DAG has only single-vertex SCCs; one big cycle is a single SCC.
The key structural fact: if you shrink every SCC into one vertex, the result (the condensation) is always a DAG. Any cycle among components would merge them into one. So "directed graph with cycles" questions often become "DAG" questions: topological order, DP over the condensation, counting sources and sinks.
Two linear-time algorithms:
- Tarjan. One DFS. Each vertex gets a discovery index
dfnand a valuelow= the smallest index reachable from its DFS subtree using edges into vertices that are still on the stack (not yet assigned to a component). A vertex withlow == dfnis the first-visited vertex (root) of its SCC; everything above it on the stack belongs to the same component. - Kosaraju. Two DFS passes. First record finishing order on the original graph. Then, on the reversed graph, start from the vertex that finished last; whatever it reaches is exactly its SCC. Repeat in decreasing finish order.
When to use it in interviews / OAs
- "Groups of mutually reachable nodes", "cities that can all reach each other", "circular dependencies".
- "Minimum number of starting vertices so that everything is reachable": count SCCs with no incoming edge in the condensation.
- "Minimum edges to add so the graph becomes strongly connected":
max(#sources, #sinks)of the condensation (0 if it is already one SCC). - "Vertex reachable from all others" (popular node): exactly one sink SCC must exist.
- Longest path or maximum collected value when vertices may be revisited: DP on the condensation, where each SCC contributes its whole value.
Template
Both versions are iterative: recursive DFS in Python fails around a thousand levels deep.
Tarjan (iterative)
def tarjan_scc(n: int, adj: list[list[int]]) -> list[int]:
# comp[v] = SCC id; ids come out in reverse topological order of the condensation
dfn = [-1] * n
low = [0] * n
on_stack = [False] * n
stack: list[int] = []
comp = [-1] * n
timer = ncomp = 0
for s in range(n):
if dfn[s] != -1:
continue
dfn[s] = low[s] = timer
timer += 1
stack.append(s)
on_stack[s] = True
work = [(s, iter(adj[s]))]
while work:
u, it = work[-1]
for v in it:
if dfn[v] == -1: # tree edge: descend into v
dfn[v] = low[v] = timer
timer += 1
stack.append(v)
on_stack[v] = True
work.append((v, iter(adj[v])))
break
if on_stack[v]: # edge into the still-open part of the DFS
low[u] = min(low[u], dfn[v])
else: # u is finished
work.pop()
if work:
p = work[-1][0]
low[p] = min(low[p], low[u])
if low[u] == dfn[u]: # u is the root of an SCC: pop it off
while True:
x = stack.pop()
on_stack[x] = False
comp[x] = ncomp
if x == u:
break
ncomp += 1
return comp
Kosaraju (iterative)
def kosaraju_scc(n: int, adj: list[list[int]]) -> list[int]:
# comp ids come out in topological order of the condensation
order, seen = [], [False] * n
for s in range(n):
if seen[s]:
continue
seen[s] = True
work = [(s, iter(adj[s]))]
while work:
u, it = work[-1]
for v in it:
if not seen[v]:
seen[v] = True
work.append((v, iter(adj[v])))
break
else:
work.pop()
order.append(u) # finishing order
radj: list[list[int]] = [[] for _ in range(n)]
for u in range(n):
for v in adj[u]:
radj[v].append(u)
comp, c = [-1] * n, 0
for s in reversed(order): # latest finisher first
if comp[s] != -1:
continue
comp[s] = c
stack = [s]
while stack:
u = stack.pop()
for v in radj[u]:
if comp[v] == -1:
comp[v] = c
stack.append(v)
c += 1
return comp
Using the condensation
def condensation(n: int, adj: list[list[int]], comp: list[int]) -> list[set[int]]:
dag: list[set[int]] = [set() for _ in range(max(comp) + 1)]
for u in range(n):
for v in adj[u]:
if comp[u] != comp[v]:
dag[comp[u]].add(comp[v]) # a set drops parallel component edges
return dag
def min_edges_to_strongly_connect(n: int, adj: list[list[int]]) -> int:
comp = tarjan_scc(n, adj)
dag = condensation(n, adj, comp)
k = len(dag)
if k == 1:
return 0
has_in = [False] * k
for a in range(k):
for b in dag[a]:
has_in[b] = True
sources = sum(not x for x in has_in)
sinks = sum(not dag[a] for a in range(k))
return max(sources, sinks)
sources alone answers "minimum number of starting vertices to reach the whole graph".
Complexity
Tarjan and Kosaraju are both O(n + m) time. Tarjan uses one pass and O(n) extra memory; Kosaraju is easier to explain but builds the reversed graph (O(n + m) more memory). Building the condensation is O(n + m).
Common pitfalls
- Recursion depth. Use the iterator-stack DFS; a chain of
10^5vertices crashes a recursive version in Python. - Updating
lowthrough finished vertices. An edge into a vertex that already belongs to a completed SCC (not on the stack) must be ignored; using it merges unrelated components. - Wrong order in Kosaraju. The second pass must run on the reversed graph, starting from the latest finishing time.
- Mixing up id orders. Tarjan's ids are reverse topological; Kosaraju's are topological. Check before running a DP over the ids.
- The single-SCC case. Adding zero edges is enough when everything is already strongly connected; the
max(sources, sinks)formula would say 1. - Undirected graphs. SCCs are a directed notion; for undirected graphs you want connected components, or bridges and articulation points for "critical" questions.
Interview variations
- Minimum vertices to reach all nodes. For a DAG it is the in-degree-0 vertices; in general it is one vertex per source SCC.
- Popular vertex. If the condensation has exactly one sink, every vertex in it is reachable from everybody; otherwise no vertex is.
- Maximum collected value on a walk. Sum values inside each SCC, then longest path on the condensation DAG.
- Functional graphs (every vertex has exactly one outgoing edge, as in "maximum employees invited to a meeting"): each component is one cycle with trees hanging into it; peeling in-degree-0 vertices (Kahn) leaves exactly the cycle vertices, no full SCC machinery needed.
- 2-SAT. Boolean constraints of the form "a or b" reduce to SCCs on an implication graph; contest material, rarely asked.