Strongly connected components
Picture
A↔B and B→C form two mutual-reachability groups, not one. C cannot return. Shrinking {A,B} into one dot leaves a one-way edge to {C}. The result is a DAG even when the original graph has cycles.
Predict
Why can the graph of components not contain a cycle?
Walk through the small example
A cycle between components would let every component on that cycle reach every other one in both directions. They would actually be one strongly connected component.
Mental model and complete Python solution
Kosaraju makes two DFS passes. First record vertices when their outgoing exploration finishes. Across an edge between distinct components, the source component has a later maximum finish time than the destination. Then reverse every edge and process vertices in decreasing finish order. The next unassigned component cannot escape to another unassigned component in the reversed graph, so one DFS collects exactly it. Unlike DSU, both passes respect arrow direction. The condensation DAG preserves inter-component reachability; an optimization inside a component still needs its own justified summary.
What the function promises: graph[u] lists directed neighbors with labels 0..n−1. Return components as lists of vertices and the corresponding condensation adjacency list. Component numbering is implementation-defined.
def strongly_connected(graph):
n = len(graph)
reverse = [[] for _ in range(n)]
for u in range(n):
for v in graph[u]:
reverse[v].append(u)
seen, finish = set(), []
for start in range(n):
if start in seen:
continue
seen.add(start)
stack = [(start, iter(graph[start]))]
while stack:
u, neighbors = stack[-1]
try:
v = next(neighbors)
if v not in seen:
seen.add(v)
stack.append((v, iter(graph[v])))
except StopIteration:
finish.append(u)
stack.pop()
component_of = [-1] * n
components = []
for start in reversed(finish):
if component_of[start] != -1:
continue
label = len(components)
component_of[start] = label
stack, members = [start], []
while stack:
u = stack.pop()
members.append(u)
for v in reverse[u]:
if component_of[v] == -1:
component_of[v] = label
stack.append(v)
components.append(members)
dag = [set() for _ in components]
for u in range(n):
for v in graph[u]:
a, b = component_of[u], component_of[v]
if a != b:
dag[a].add(b)
return components, dag
groups, dag = strongly_connected([[1], [0, 2], []])
assert {frozenset(group) for group in groups} == {frozenset({0,1}), frozenset({2})}
assert sum(map(len, dag)) == 1
assert strongly_connected([]) == ([], [])
Work and storage: O(V+E) expected time including set-based edge deduplication, and O(V+E) space. Explicit stacks avoid recursion-depth failure on long chains.
When to recognize it: Mutual reachability forms groups. Shrinking every group turns the remaining connections into a DAG.
Check before applying it: Union-find ignores arrow direction. Also, compressing a cycle does not automatically solve an optimization problem inside it.
Change one rule
Each node gives a reward at most once, revisits are allowed, and all rewards are nonnegative. Can a component’s reward be its sum?
See what changes
Yes: inside a strongly connected component, a walk can visit all members and then reach a chosen exit, revisiting nodes when needed. Sum its nonnegative rewards and use DAG DP between components. If revisits are forbidden or rewards/cost rules change, that sum may no longer describe a feasible internal walk.
Connect
Course Schedule II: The condensed graph can be evaluated in topological order.
Parallel Courses III: Use DAG DP only after defining what each component can safely summarize.