{} Coding Interviews · ch.16 · graphs II: topo sort & union-find
🧩 Part IV · Graphs & Exhaustive Search · chapter 16 / 24

Ship what's unblocked,
merge what touches

Two bookkeeping tricks own this chapter: a queue that repeatedly ships anything with no remaining prerequisites, and two humble arrays that answer "are these connected?" in near-constant time. Between them they cover every build-order and every merge-the-groups problem in the bank.

Chapter 15 gave you graphs where the question was reachability — flood the grid, count the islands. This chapter's graphs ask two different questions. First: the edges have direction, they mean "this before that", and you must produce a legal order — or prove none exists. Second: edges keep arriving, and you must answer "are these two in the same group yet?" over and over, faster than re-running DFS every time.

Different questions, different machines. The first is topological sort, and its best interview form is Kahn's algorithm — barely more than the BFS queue you already own. The second is union-find, the smallest data structure in this book: two arrays and about twelve lines, running at effectively O(1) per operation once you add its two famous optimizations. Both show up constantly; both are ten-minute installs. Let's install them.

1The arrow means "before" — dependencies form a DAG

Every dependency system you've ever cursed at — build tools, package managers, course catalogs, spreadsheet cells — is the same picture: nodes, plus directed edges where u → v reads "u must happen before v". If that picture has no directed cycle, it's a DAG — a directed acyclic graph — and a DAG always admits at least one topological order: a line-up of all the nodes where every arrow points forward. Usually many such line-ups; the interviewer will accept any of them.

Two things to internalize before any code:

  • A topological order exists if and only if there is no cycle. One directed cycle and the whole thing is unschedulable: each member of the cycle politely waits for the previous one, forever. Deadlock. Most "can you finish?" problems are secretly just asking "is there a cycle?"
  • The order is a property of the arrows, not the nodes. Nothing about a course called "CS201" makes it third; what makes it third is that its incoming arrows come from things you've already shipped. That framing — count incoming arrows — is the entire algorithm of Section 2.

The number of incoming edges of a node has a name, in-degree, and it deserves a friendlier one: it's the node's remaining-blockers counter. In-degree zero means "nothing is stopping you — go."

🎯
The tell: "prerequisites", "build order", "must be taken before", "can you finish all n?", "return a valid sequence or say it's impossible" — that's topological sort, and "impossible" is always spelled c-y-c-l-e. If the pairs are directional, don't even glance at union-find; it can't see arrows.

2Ship whatever has no blockers — Kahn's algorithm

Here's the whole algorithm, in the words you'd use at a whiteboard: repeatedly ship anything with no remaining prerequisites. Shipping a node removes its outgoing arrows, which may unblock other nodes; queue those up and keep going. That's Kahn's algorithm — a to-do list that eats itself:

  • Build an adjacency list (prereq → things it unlocks) and an indegree[] array in one pass over the pairs.
  • Seed a queue with every node whose in-degree is 0 — they're buildable right now.
  • Pop a node, append it to the order, and decrement the in-degree of everything it points at. Any neighbor that hits 0 joins the queue.
  • When the queue empties, either you shipped everything (there's your order) or you didn't (Section 3).

Cost: O(V + E) — every node enters the queue once, every arrow gets decremented once; "touch each thing once" pricing. The queue is literally Chapter 15's BFS queue with a stricter bouncer: instead of "have I seen you?", admission is "has everything before you shipped?". Try it — take the eight courses below in any legal order you like, then flip the cycle on and watch the same clicking ritual jam.

Interactive · the prereq untangler Click a pulsing (zero in-degree) course to take it; badges count remaining blockers
click any pulsing course
shipped
0 / 8
unblocked right now
2
verdict
scheduling…
💡
Notice what you never did in that widget: look ahead. No planning, no cleverness — just "who's unblocked now?" repeated. Kahn's is a greedy algorithm that happens to be always right, because shipping an unblocked node can never hurt anyone downstream. That's why it survives in your memory: there's almost nothing to remember.

3When the queue starves — a cycle is a deadlock

Kahn's algorithm has the most elegant failure mode in the book: it just… stops early. If the queue empties while nodes remain, every leftover node has in-degree ≥ 1 — each one is waiting on another leftover. That's a cycle (or several), and your cycle test is one comparison: shipped == n. No colors, no recursion stacks, no cleverness. This single check is the whole answer to Course Schedule Medium, and appending each shipped node to a list upgrades it to Course Schedule II Medium — same loop, keep the receipts.

There is a DFS alternative, and it's worth one sentence in the room: mark nodes white/grey/black (unvisited / on the current recursion path / done), and a grey→grey edge is a back-edge, i.e. a cycle. It's the same recursion shape you drilled in Chapter 11, and post-order DFS reversed is also a valid topo order. Fine algorithm. But under interview pressure, three colors and a recursion stack offer more places to fumble than a queue and a counter — most people should say "I'll use Kahn's" and move on.

⚠️
The direction trap: LeetCode's Course Schedule hands you pairs as [course, prereq] — the arrow you want runs prereq → course, i.e. from index 1 to index 0. Wiring it backwards still compiles, still runs, and still passes the no-cycle examples (a reversed DAG is still a DAG) — it just emits orders that are exactly wrong. Say the direction out loud while you build the adjacency list; it's a free correctness proof.

4An alphabet hiding in a word list — Alien Dictionary

The hardest part of many topo-sort problems is noticing the graph at all. Alien Dictionary Hard is the canonical disguise: you get a list of words claimed to be sorted by some unknown alphabet, and you must recover the alphabet. No arrows anywhere in sight — until you remember what "sorted" means. If "ett" comes before "rftt", then at the first position where they differ, the earlier word's letter must come first in the alphabet: e → r. That's an edge.

So the solve is a pipeline: compare each adjacent pair of words (only adjacent — non-adjacent pairs add nothing new), extract one edge per pair from the first mismatch, then run plain Kahn's on the letters. If the topo sort deadlocks, the claimed dictionary contradicts itself. One edge case earns you real points: if a word is a strict prefix of the word before it ("apple" then "app"), no mismatch letter exists and no alphabet can fix it — return impossible before sorting anything.

Interactive · the alien decoder Adjacent word pairs → first-mismatch edges → Kahn's on the letters
pairs compared
0 / 4
edges found
0
alphabet so far
verdict
extracting edges…
🎯
The tell: an unknown total order plus a pile of pairwise "this before that" evidence — sorted word lists, match results, recipe steps — is topo sort wearing a costume. Your first move is never sorting; it's harvesting the evidence into edges, then letting Kahn's do the ordering.

5Two arrays that know who's connected — union-find

Now drop the arrows. The second family of problems hands you undirected relations — "these two accounts share an email", "this wire joins these two houses" — and asks about groups: how many are there? Are these two in the same one? Which edge was one edge too many? You could answer with Chapter 15's DFS… once. But these problems typically stream their edges, asking questions as the graph grows, and re-flooding the whole graph per question is how an O(n) idea becomes an O(n²) submission.

Union-find (a.k.a. disjoint set union, DSU) is the tool built for exactly this. Its entire state is one array — parent[] — plus a helper. Every node points at some node in its group; follow the pointers up and you reach a node that points at itself. That's the root: the group's elected representative. The API is two functions:

  • find(x) — follow parents up from x until a node is its own parent; return that root. Two nodes are connected iff their finds agree.
  • union(a, b) — find both roots; if they differ, point one root at the other. Two groups just became one. If they're already equal, the edge you're adding connects a group to itself — remember that fact, it's a whole problem category (Section 7).

That's it. Start with parent[i] = i — n groups of one — and feed edges in. The component count starts at n and drops by exactly one on every successful union; no traversal ever happens. It's not a graph algorithm so much as group-membership bookkeeping.

🎯
The tell: "are these two connected", "merge the accounts/groups", "how many components remain", "find the redundant edge", or edges arriving one at a time with questions in between — that's union-find. DFS answers connectivity for a graph frozen in time; union-find answers it for a graph that's still happening.

6Flatten as you walk — path compression & union by size

The naive version has a lurking disaster: nothing stops the parent pointers from forming a chain. Union 0→1, 1→2, … 8→9 the lazy way and find(0) walks nine hops — the "tree" is a linked list, and finds cost O(n). Two one-line fixes make that impossible:

  • Union by size (or rank): when merging, hang the smaller tree's root under the bigger tree's root. A node's depth now only grows when its whole tree gets absorbed by one at least as big — that can happen at most log n times. Trees stay bushy by law.
  • Path compression: while find walks up, make every node it passes point (nearly) straight at the root. Each query leaves the structure flatter than it found it — the data structure is self-healing.

Together they push the amortized cost per operation to O(α(n)) — the inverse Ackermann function, a value that is ≤ 4 for any n that fits in this universe. Say "effectively constant" in the room and, if asked, name the function; that exchange is worth more than most solved problems. Build a worst-case chain below, then run one compressed find through it and watch it pancake.

Interactive · the union-find forest Union mode: click two nodes. Find mode: click one and watch compression flatten its path
pick two nodes to union
components
10
tallest tree (height)
0
last find (hops)
⚠️
The bare-DSU trap: writing union-find without compression or by-size is the classic way to turn an accepted idea into a Time Limit Exceeded. Both fixes together are three extra lines. If you memorize exactly one code artifact from this chapter, make it the twelve-line DSU in Section 9 — with the optimizations baked in, not bolted on.

7One machine, four problems — the connectivity family

Once the DSU exists, its canonical problems are almost embarrassingly short — each one is the machine plus a single observation:

  • Number of Connected Components Medium — start the counter at n; every union that returns "merged" decrements it. Answer: the counter. (You could DFS this one — the graph is static — but the DSU version is five lines and no recursion.)
  • Redundant Connection Medium — feed edges in order; the first edge whose two endpoints already share a root is the one closing a cycle. Return it. The "union returned false" branch is the answer.
  • Graph Valid Tree Medium — a tree on n nodes is exactly: n − 1 edges and no edge ever redundant (equivalently: one component at the end). Two checks, both free once the DSU runs.
  • Accounts Merge Medium — union accounts that share an email; each root collects its group. The interview twist is mapping strings to ids first — DSU over a dictionary of emails, not integers.

Outside the interview bank, this same machine is the heart of Kruskal's minimum-spanning-tree algorithm — sort edges by weight, union greedily, skip redundant ones. If a system-design-flavored interviewer asks where you'd use DSU "for real", that's your answer.

💡
Spot the shared skeleton: every one of these is "loop over edges, call union, react to the return value". The problems differ only in which return value they care about — the merges (components), the non-merges (redundant edge), or the final tally (valid tree). Learn the machine once; the problems are configuration.

8Arrows or handshakes — picking the tool in ten seconds

Part IV now holds three graph tools, and the problem statement tells you which one it wants before you've drawn anything:

  • Directed edges meaning "before", and you need an order or a "can you finish" verdict → topological sort (this chapter). Cycle = impossible.
  • Undirected edges meaning "same group", especially arriving over time → union-find (this chapter). Redundant union = cycle, for free.
  • A frozen graph and a reachability/region question → Chapter 15's DFS/BFS. And the moment edges grow weights and the question says "cheapest", the BFS queue needs to become a priority queue — that's Dijkstra, one page-turn away in Chapter 17.

One symmetry worth saying out loud in an interview, because it sounds like mastery and is: both of this chapter's tools are cycle detectors — topo sort catches directed cycles (the queue starves), union-find catches undirected ones (the union comes back redundant). When Chapter 23's decision tree asks "does the problem smell like a cycle?", these are the two leaves it's choosing between.

The pattern, as a whiteboard skeleton:

  1. 1Read the edges: directional "before/after" → topo sort. Symmetric "connected/merged" → union-find.
  2. 2Topo: one pass over the pairs builds adj[prereq] → courses and indegree[] — say the arrow direction out loud.
  3. 3Queue every node with in-degree 0; they're buildable now.
  4. 4Loop: pop, record, decrement each neighbor; any neighbor hitting 0 joins the queue.
  5. 5Recorded < n? Cycle → return impossible. Otherwise the record is your order.
  6. 6DSU: parent[i] = i, size[i] = 1; find walks to the root, compressing as it goes.
  7. 7union hangs the smaller root under the bigger; roots already equal → redundant edge / cycle detected.
  8. 8Components = n − successful unions; valid tree = n − 1 edges and zero redundant ones.

9A queue and two arrays — the whole chapter in code

Course Schedule Medium first: given numCourses and pairs [course, prereq], can you take everything? It's Kahn's algorithm verbatim — build, seed, drain, compare the count. Then the chapter's other half: the complete DSU with path compression and union by size, the twelve lines you'll reuse across the entire connectivity family. Read the Python for the shape and flip to Scala to see the same two machines with types on.

💡
Course Schedule II is the same Kahn loop collecting instead of counting: append each popped course to order and return it — or [] when the count comes up short. When one extra list upgrades a solution to the sequel problem, mention it; interviewers hear "I see the family, not the problem."