Lecture 21
What Is Topological Sort?
A topological sort is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u appears before vertex v in the ordering.
Topological sort only exists for DAGs — any cycle makes a consistent ordering impossible. The classic example is course prerequisites: if course A must be taken before course B, then A must appear before B in any valid schedule.
Kahn's Algorithm
Kahn's algorithm computes a topological sort using BFS. The key insight is that every DAG has at least one vertex with in-degree 0 (no prerequisites). Process those first; removing them may expose new zero-in-degree vertices.
Steps:
- Compute the in-degree of every vertex.
- Enqueue all vertices with in-degree 0.
- While the queue is non-empty: dequeue a vertex, append it to the result, decrement the in-degree of each of its neighbors; if a neighbor reaches in-degree 0, enqueue it.
- If the result contains fewer than V vertices, the graph has a cycle — no topological ordering exists.
DiGraph Implementation
Unlike the undirected Graph from lecture 20, add_edge here only records the edge in one direction and tracks the in-degree of the destination.
Kahn's trace on a 5-node DAG:
Topological order is not unique. Both 0 1 2 3 4 and 0 2 1 3 4 are valid orderings for the
graph above — vertex 1 and vertex 2 have no dependency between them, so either can come first.
Kahn's processes them in FIFO queue order, producing one valid ordering.
Always check result.size() == V after the loop. If the result is shorter, a cycle prevented
some vertices from ever reaching in-degree 0 — the caller has no way to know the sort is
incomplete unless you check.
What Is Union Find?
Union Find (also called Disjoint Set Union) tracks a collection of elements partitioned into disjoint sets. It supports two operations:
find(x)— returns the representative (root) of x's set.unite(x, y)— merges the sets containing x and y.
Two elements are in the same connected component if and only if find(x) == find(y).
Union Find represents each component as a tree stored in the parent_ array. Each node points to its parent; roots point to themselves. find(x) walks up to the root; unite(x,y) links two roots together.
After unite(0,1), unite(1,2), and unite(4,5):
Roots — 0, 3, 4 — have no incoming arrow. find(2) follows 2 → 0 and returns 0. find(5) follows 5 → 4 and returns 4. Since find(2) ≠ find(5), the two elements are in different components.
After unite(2,3), node 3 (or its root) is attached under 0:
Now find(3) also returns 0 — 0, 1, 2, and 3 all belong to the same component.
Union Find Implementation
The underlying structure is a parent_ array: each element points to its parent, and the root of a set points to itself.
Path Compression and Union by Rank
Two optimizations keep operations near-constant amortized:
Path compression — when find walks up to the root, it rewires every node along the path to point directly to the root. Future find calls on those nodes take O(1).
Union by rank — always attach the shallower tree under the root of the taller tree. The rank_ array tracks tree height. This prevents long chains from forming.
| Operation | Naive find | With path compression + union by rank |
|---|---|---|
find (worst case) | O(n) | O(α(n)) ≈ O(1) amortized |
unite | O(n) | O(α(n)) ≈ O(1) amortized |
α(n) is the inverse Ackermann function — it grows so slowly that it is effectively constant for any realistic input size.
Exercises
Q1.
Without running the code, predict what this program prints.
Q2.
Without running the code, predict what this program prints.
Q3.
Each snippet has a bug. Identify what's wrong.
Q4.
Implement toposort() using Kahn's algorithm.
Q5.
Implement find and unite for the union find below.
Practice Problems
- Course Schedule — Medium
- Course Schedule II — Medium
- Number of Provinces — Medium
- Redundant Connection — Medium