Algorithm tutorials
Topological Sort (Kahn's Algorithm and DFS)
Order the vertices of a directed acyclic graph so every edge points forward, detect cycles along the way, and run DP over a DAG for longest paths and scheduling.
Intuition
A topological order lists the vertices of a directed graph so that every edge u -> v has u before v: prerequisites before courses, libraries before the code that links them. Such an order exists exactly when the graph is a DAG (directed acyclic graph). If there is a cycle, every vertex on it would have to come before itself.
Two classic ways to build one:
- Kahn's algorithm (BFS flavor). A vertex with in-degree 0 has no unmet prerequisites, so it can go first. Output it, delete its outgoing edges (decrement the in-degree of its targets), and repeat. If you get stuck before outputting all
nvertices, every remaining vertex still has an incoming edge from another remaining vertex, so the remainder contains a cycle. - DFS (finishing order). In a DFS, a vertex finishes only after everything reachable from it has finished. Reversing the finishing order therefore puts every vertex before its descendants.
Every DAG has at least one source (in-degree 0) and one sink (out-degree 0). Once you have an order, any "best path" question becomes a one-pass DP: process vertices in topological order and push values along out-edges. That gives longest paths, shortest paths (even with negative weights) and path counts in O(n + m).
When to use it in interviews / OAs
- Prerequisites, dependencies, build or install order, task scheduling: "can all courses be finished?", "return a valid order".
- Deriving an order from comparisons: alien dictionary, sequence reconstruction.
- "Minimum number of semesters / rounds" with unlimited parallelism: count the layers of Kahn's algorithm.
- Earliest finish time when tasks have durations (critical path): DP in topological order.
- Longest path or number of paths in a DAG, including implicit DAGs such as "longest increasing path in a matrix" (edges go from smaller to larger cells).
Template
Kahn's algorithm
from collections import deque
import heapq
def topo_kahn(n: int, edges: list[tuple[int, int]]) -> list[int] | None:
# an edge (u, v) means u must come before v; returns None if there is a cycle
adj: list[list[int]] = [[] for _ in range(n)]
indeg = [0] * n
for u, v in edges:
adj[u].append(v)
indeg[v] += 1
q = deque(v for v in range(n) if indeg[v] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if len(order) == n else None
DFS, reverse finishing order
Gray marks vertices on the current path, so an edge into a gray vertex is a cycle.
def topo_dfs(n: int, adj: list[list[int]]) -> list[int] | None:
color = [0] * n # 0 = unvisited, 1 = on the current path, 2 = finished
finished = []
for s in range(n):
if color[s]:
continue
color[s] = 1
stack = [(s, iter(adj[s]))]
while stack:
u, it = stack[-1]
for v in it:
if color[v] == 1:
return None # back edge: cycle
if color[v] == 0:
color[v] = 1
stack.append((v, iter(adj[v])))
break
else:
color[u] = 2
finished.append(u) # u finishes after everything it points to
stack.pop()
return finished[::-1]
Lexicographically smallest order
Replace the queue with a min-heap: among all vertices that are currently free, always take the smallest.
def topo_smallest(n: int, edges: list[tuple[int, int]]) -> list[int] | None:
adj: list[list[int]] = [[] for _ in range(n)]
indeg = [0] * n
for u, v in edges:
adj[u].append(v)
indeg[v] += 1
heap = [v for v in range(n) if indeg[v] == 0]
heapq.heapify(heap)
order = []
while heap:
u = heapq.heappop(heap)
order.append(u)
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
heapq.heappush(heap, v)
return order if len(order) == n else None
DP over a DAG: earliest finish time
Tasks have durations, edges are prerequisites, any number of tasks can run in parallel. A task starts when its last prerequisite ends, which is a longest-path computation done in Kahn order.
def min_finish_time(n: int, prereq: list[tuple[int, int]], time: list[int]) -> int:
adj: list[list[int]] = [[] for _ in range(n)]
indeg = [0] * n
for u, v in prereq:
adj[u].append(v)
indeg[v] += 1
start = [0] * n # earliest start of each task
q = deque(v for v in range(n) if indeg[v] == 0)
total = 0
while q:
u = q.popleft()
done = start[u] + time[u]
total = max(total, done)
for v in adj[u]:
start[v] = max(start[v], done)
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return total
Deriving the graph: alien dictionary
Compare each pair of adjacent words; the first differing letter gives one edge. A longer word placed before its own prefix makes the input invalid.
def alien_order(words: list[str]) -> str:
letters = {ch for w in words for ch in w}
adj: dict[str, set[str]] = {ch: set() for ch in letters}
indeg = {ch: 0 for ch in letters}
for a, b in zip(words, words[1:]):
for x, y in zip(a, b):
if x != y:
if y not in adj[x]: # do not count a repeated edge twice
adj[x].add(y)
indeg[y] += 1
break
else:
if len(a) > len(b): # "abc" listed before "ab"
return ""
q = deque(sorted(ch for ch in letters if indeg[ch] == 0))
out = []
while q:
x = q.popleft()
out.append(x)
for y in sorted(adj[x]):
indeg[y] -= 1
if indeg[y] == 0:
q.append(y)
return "".join(out) if len(out) == len(letters) else ""
Complexity
Kahn's algorithm and the DFS version are both O(n + m) time and O(n + m) space for the adjacency list. The heap version costs O(m + n log n). DAG DP adds only O(n + m).
Common pitfalls
- Edge direction. "To take course
ayou must first takeb" is the edgeb -> a. Flipping it still produces an order, just the reverse one, which is wrong. - Skipping the cycle check. Kahn's algorithm silently returns a short list when there is a cycle; compare its length with
n. - Preorder instead of reversed postorder in the DFS version. Preorder is not a topological order: with edges
a -> bandc -> a, starting atagives the preordera, b, c, yetcmust precedea. - A plain visited set in DFS cannot tell a cycle from a revisit; keep the "on the current path" state.
- Double-counting in-degrees when the same constraint appears twice but the adjacency is a set (alien dictionary).
- Assuming the order is unique. Many orders are usually valid; if a specific one is expected (smallest, unique), say how you pick.
Interview variations
- Course schedule I / II. Feasibility = no cycle; output = Kahn order.
- Parallel courses. Minimum semesters with unlimited parallelism = number of Kahn layers (process the queue level by level); with durations it is the earliest-finish DP above.
- Sequence reconstruction / unique order. The topological order is unique iff the queue never holds more than one vertex at a time.
- Critical path. Compute earliest start forward and latest start backward in topological order; tasks with equal values have zero slack and form the critical path.
- Implicit DAGs. Longest increasing path in a matrix, longest string chain, recipes from supplies: build edges from the rule, then Kahn or memoized DFS.
- Two-level ordering. "Sort items by groups respecting dependencies": topologically sort the groups, then the items inside each group.