Exercises — Strongly Connected Components (SCC) — Kosaraju's algorithm, Tarjan's algorithm
Before we start, three conventions used everywhere on this page:
The running example graph for several problems is drawn in figure s01 and also written out here in full so you can reconstruct it without the image:

Level 1 — Recognition
L1.1
Using the running graph (figure s01), which of these pairs are in the same SCC? (a) (b) (c) (d)
Recall Solution
An SCC needs a path both ways.
- (a) — same SCC. directly, and back. Round trip exists.
- (b) — different. You can go (forward), but there is no way back from to : from every edge stays inside .
- (c) — same SCC. and directly.
- (d) — different. Same reason as (b): has no exit edge back to .
So the SCCs are and . Same-SCC pairs: (a) and (c).
L1.2
A directed graph has SCCs with condensation edges , , . Question: How many nodes and edges does the condensation have, and is it a DAG?
Recall Solution
The condensation contracts each SCC to one node, so it has nodes: . It has the listed edges. A DAG (Directed Acyclic Graph) is a directed graph with no cycle. Trace: from you can reach and , but nothing loops back to ; has no outgoing edge. No cycle ⇒ yes, it is a DAG. (The condensation is always a DAG — if it had a cycle, those SCCs would be mutually reachable and would merge into one.)
Level 2 — Application
L2.1 (Kosaraju, DFS1)
Run DFS1 (the first pass, on the original graph ) on the running graph, starting at vertex and preferring the smallest unvisited neighbour. Record the finish order — the order vertices are pushed when DFS returns from them.
Recall Solution
Why push on return, not on entry? A vertex is "finished" only after all its descendants finish; pushing on return is what records that. Walk it:
- Enter → go to → go to . From neighbours are (visited) and ; go to → go to .
- From the only edge is (visited). finishes → push 5.
- Back at : neighbour visited. finishes → push 4.
- Back at : all neighbours done. finishes → push 3.
- Back at , then : finish → push 2, push 1.
Finish order (push sequence): . Reversed (decreasing finish time): . This reversed list is the exact order DFS2 will use.
L2.2 (Kosaraju, DFS2)
Reverse the graph and run DFS2 (second pass, on ) in the order from L2.1. List the SCCs.
Recall Solution
Reversed edges: . Process order :
- Start 1 (unvisited). In : , and (visited). The whole tree grown from is = one SCC. Why does the tree stop here? Edge points into this group, and DFS only follows arrows forward, so we can never step out to .
- Next unvisited is 4: and (visited). Tree = one SCC.
SCCs: and . Why one tree = one SCC? Because DFS2 always starts in a sink SCC of (proved in L5.1), which has no outgoing edges to unfinished components, so a single exploration captures exactly one component.
L2.3 (Tarjan, one pass)
Run Tarjan on the running graph starting at . Give final disc and low for every node (index starts at ), and the emitted SCCs. Recall the update rules: for a tree edge take after recursing; for an edge to a vertex with onStack true take .
Recall Solution
Process edges in the order they appear. Discovery order is , so
and initially for each. Stack grows , all onStack = true.
Step through, deepest first (recursion returns bottom-up):
- At : its only edge . Is on the stack? Yes. So this is a back edge, use
disc: . Loop done. Check root: ⇒ is not a root, do not pop. Return to . - At (after recursing into ): was a tree edge , so use
low: . Now check root: ⇒ root at 4. Pop the stack down to : pop , pop ⇒ SCC . Both getonStack=false. Return to . - At (after recursing into ): first the tree edge returns: . Then edge : is on the stack? Yes ⇒ back edge, use
disc: . Loop done. Root check: ⇒ not a root. Return to . - At : tree edge returns: . Not a root (). Return to .
- At : tree edge returns: . Root check: ⇒ root at 1. Pop down to : pop , pop , pop ⇒ SCC .
Why does mark a root? It means cannot reach any vertex discovered earlier than itself (without leaving the current stack). So no ancestor is reachable back — is the highest (earliest-discovered) vertex of its SCC, i.e. its entry point. Everything sitting above on the stack was reached from and can loop back to it, so they form exactly one component.
Final: , . SCCs: , — same answer as Kosaraju, in one pass.
Level 3 — Analysis
L3.1 (Why the asymmetry?)
In Tarjan, why do we update with for a tree edge but only for a back/cross edge to a node on the stack? Give a single concrete small graph where using for the non-tree edge would give a wrong answer.
Recall Solution
A tree edge means is a fresh child whose whole subtree we are about to explore; its final summarises everything reachable below , so we absorb it fully. A back/cross edge to an on-stack node is a single shortcut — Tarjan's rule says you may climb using at most one such edge. Using records exactly "I reached a vertex first discovered at time ." Using would let inherit somewhere can reach through further edges, possibly a vertex in a finished, different SCC — wrongly merging two components.
Concrete minimal counterexample. Four vertices, edges: True SCCs: and (each is a 2-cycle; is a one-way bridge, and is a cross edge back to the first component). Start at : discovery with .
- Correct Tarjan: at , edge — but by the time we test it, whether is on the stack decides everything. Under a start that finishes -style ordering, (tree/back inside ) gives ; becomes root ⇒ SCC correctly, provided is ignored because that path must not import component .
- Buggy version uses for the cross edge : since , we would set , so never becomes a root and gets glued onto — one wrong giant SCC instead of two. That is the merge the
disc+onStackrule prevents.
L3.2 (Finish-time inequality)
State and justify: if the condensation has an edge , then (finish times in DFS1).
Recall Solution
Let be the finish time of vertex .
- Case 1 — DFS enters before touching any of . Since exists and is strongly connected, DFS from the first -vertex reaches all of and then all of before it can return. So that first -vertex finishes after everything in ⇒ its finish is the max, larger than 's max.
- Case 2 — DFS enters first. The condensation is a DAG and the edge is , so there is no path . DFS from therefore finishes all of without ever touching . Later DFS starts inside , so all of finishes after all of .
Both cases give : the SCC that is "upstream" in the DAG finishes later. This is exactly why picking the highest-finish vertex on the reversed graph lands you in a sink SCC there.
L3.3 (Spot the bug: omitting onStack)
A student's Tarjan forgets the onStack check and always does low[u]=min(low[u],disc[v]) for any already-visited (even one whose SCC is already finished). Give a concrete small graph, trace both the correct and buggy runs, and show how two SCCs get merged.
Recall Solution
Graph (three vertices):
True SCCs: (mutual) and (nothing returns to ).
Start DFS at 3: , stack , onStack[3]=true.
- Edge : unvisited ⇒ tree edge, recurse. , stack .
- Edge : recurse. , stack .
- Edge : is on the stack ⇒ back edge, . Not a root. Return.
- Back at : tree edge returns ⇒ root at 1, pop . Now
onStack[1]=onStack[2]=false— SCC is finished.
- Edge : recurse. , stack .
- Back at : tree edge returns , so ⇒ root at 3, pop . SCC . Correct result: .
Now make the bug bite. Reorder so the offending edge is a non-tree edge into an already-popped vertex. Take starting at : and are still the true SCCs.
- , stack . From : edge (on stack) ⇒ . Then edge : recurse, , stack .
- From : edge . Is on the stack? Yes here, so both correct and buggy set ; is not a root, so gets glued to — but that is only because genuinely is on the stack (this graph really has all in one SCC via ).
The clean demonstration is the first graph run with the bug forced: if the code never checks onStack, then when we return to and see the finished vertex (already popped, onStack[1] should stop us), the buggy code still does ? No — , so it stays . The genuine merge appears when a later vertex has an edge to a popped, smaller-disc vertex: min(low[w],disc[popped]) drops below , so never becomes its own root and its whole component fuses with the finished one. The onStack guard is precisely what forbids consulting a popped vertex.
Level 4 — Synthesis
L4.1 (SCC → topological order)
After finding SCCs you build the condensation (a DAG). Give the SCCs of the running graph a valid topological order and explain what a topological order guarantees.
Recall Solution
SCCs: and . The only condensation edge is (from ). A topological order lists nodes so every edge points forward. Here the unique order is . Guarantee: in a topological order, for every edge , appears before . This exists iff the graph is a DAG — which the condensation always is. So "SCC then topo-sort the condensation" is the standard recipe to impose a valid processing order on any cyclic directed graph.
L4.2 (2-SAT)
In 2-SAT we build an implication graph on literals and . The formula is satisfiable iff no variable has and in the same SCC. Suppose after running Tarjan you find SCC ids and . Is forced true or false, and what is the assignment rule?
Recall Solution
First define the id convention. Tarjan emits an SCC (pops it) as soon as its root is found. Because a root is only recognised after everything it can reach has been explored and popped, sink SCCs of the condensation are emitted before source SCCs. If we number SCCs in emission order, then a larger id means "emitted later" = "closer to a source" = earlier in topological order. Equivalently, Tarjan's emission order is a reverse topological order of the condensation. This is exactly why the 2-SAT rule can read off truth values directly from ids without a separate topo-sort.
- Satisfiability check: , so and are in different SCCs ⇒ no contradiction for this variable.
- Assignment rule: set the literal whose SCC is emitted later (larger Tarjan id) to false, and its negation to true. Here , so (larger id) is false, hence . The key point: SCC membership decides satisfiability, SCC ordering decides the actual truth values.
L4.3 (Directed vs undirected structure)
Bridges and Articulation Points and Union-Find describe undirected connectivity. Explain why you cannot use Union-Find to compute SCCs directly.
Recall Solution
Union-Find merges two elements whenever any connection exists between them; it only knows "connected or not," with no direction. If you union(u,v) for every directed edge , you get the weakly connected components (treat arrows as undirected) — which can be strictly larger than SCCs. Example: with no return edge puts in one Union-Find set but two different SCCs. Union-Find has no way to represent "reachable one way but not the other," so it cannot detect the mutual-reachability requirement. SCC needs the DFS ordering (Kosaraju/Tarjan); Union-Find is the wrong tool for directed cycles.
Level 5 — Mastery
L5.1 (Prove one tree = one SCC)
Prove Kosaraju's core claim: each DFS tree of the second pass (DFS2, on , in decreasing finish order) equals exactly one SCC.
Recall Solution
Let be the root of a DFS2 tree; it is the highest-finish unvisited vertex, so among all not-yet-assigned SCCs its own SCC has the largest max-finish. By L3.2's inequality, is a source in the (remaining) original condensation — no unassigned SCC has an edge into . Reverse all edges: in , becomes a sink among unassigned SCCs — every edge from to another unassigned SCC now points into . So a DFS from in :
- Reaches every vertex of (because is strongly connected — reversing edges keeps it strongly connected).
- Cannot escape to any other unassigned SCC (all their connecting edges now point inward).
Therefore the tree rooted at is exactly . Remove it and repeat on the next-highest finisher. By induction every SCC is captured as one tree. ∎
L5.2 (Design)
You are given a directed graph and queries "is there a path ?" (reachability). Design an -ish preprocessing scheme using SCCs. Sketch it.
Recall Solution
- Compute SCCs (Tarjan, ). Give each vertex its SCC id.
- Build the condensation DAG on the SCC ids ().
- Topologically sort the DAG. For dense reachability, compute a reachability bitset per SCC in reverse topological order: . This costs with bitsets but answers each query in : "?" "".
- Why SCCs first? Inside an SCC every vertex reaches every other, so all reachability questions collapse to the DAG level — far fewer nodes, and a DAG admits the clean topological DP above. Trying reachability on the raw cyclic graph would revisit cycles endlessly.
Correctness: in iff in the condensation, because SCC contraction preserves exactly the reachability relation.
L5.3 (Edge cases: self-loops and isolated vertices)
How do Kosaraju and Tarjan handle (a) a self-loop , and (b) an isolated vertex (no incident edges)? What SCC does each produce?
Recall Solution
(a) Self-loop . A single vertex is always trivially strongly connected to itself (the empty path reaches from ), so is an SCC whether or not the self-loop exists. The self-loop adds nothing to SCC structure.
- Kosaraju: DFS just marks visited on entry; the loop edge leads to an already-visited vertex and is ignored. ends up in its own tree unless a real cycle through other vertices includes it.
- Tarjan: edge finds on the stack, so — no change. still satisfies and is popped as the singleton SCC . No infinite loop, because is already
visited/onStackand we never recurse into a visited vertex. (b) Isolated vertex. It has no outgoing and no incoming edges. - Kosaraju: the outer
for each unvisited uloop eventually starts DFS1 at it; it finishes immediately. In DFS2 it forms a tree of size one ⇒ SCC . - Tarjan: the outer loop calls
dfs(u); with no edges, immediately ⇒ pop singleton SCC . Takeaway: every vertex belongs to some SCC (at minimum itself). Both algorithms need an outer loop over all vertices precisely so isolated vertices and disconnected pieces are not missed.
Recall One-line self-check
Kosaraju needs which special order for the second pass? ::: Decreasing finish time on the reversed graph.
Tarjan marks an SCC root when? ::: low[u] == disc[u], then pop the stack down to and including u.
What is the role of onStack[u] in Tarjan? ::: A flag that is true while u is on the stack; it restricts low updates to the current unfinished component, so back edges into finished SCCs are ignored.
Why can't Union-Find compute SCCs? ::: It only sees undirected connectivity; it cannot express one-way vs round-trip reachability.
What SCC does an isolated vertex or a self-loop vertex form? ::: A singleton SCC containing just that vertex.