Topological Sort: Ordering a Directed Acyclic Graph
Given a set of tasks where some must be completed before others, how do you find a valid execution order? This is exactly the problem that Topological Sort solves. It takes a Directed Acyclic Graph (DAG) and produces a linear ordering of its vertices such that for every directed edge u→v, vertex u appears before vertex v. In this post we’ll explore two classic algorithms — Kahn’s BFS approach and the DFS approach — with step-by-step visual walkthroughs, an interactive animation, C++ implementations, and competitive programming applications.
What Is Topological Sorting?
A topological ordering of a directed graph is a linear sequence of all its vertices such that if there is an edge from vertex u to vertex v, then u appears before v in the ordering. This ordering is only possible when the graph has no directed cycles — that is, the graph must be a DAG (Directed Acyclic Graph).
Consider this DAG with 7 nodes representing course prerequisites:
Edges: 0→1, 0→2, 1→3, 1→4, 2→4, 2→5, 3→6, 4→6, 5→6. One valid topological ordering is [0, 1, 2, 3, 4, 5, 6]. Another is [0, 2, 5, 1, 4, 3, 6]. Both respect all edge directions.
Real-World Examples
- Build systems: Compiling source files in dependency order (e.g., Makefiles). A file must be compiled only after all files it depends on are compiled.
- Task scheduling: Ordering tasks so that every task runs after its prerequisites are complete.
- Course prerequisites: Planning a semester schedule where each course is taken only after its prerequisites.
- Package managers: Installing packages in the right order so that dependencies are satisfied (e.g., apt, npm).
- Spreadsheet evaluation: Computing cell values in an order that respects formula dependencies.
Formal Properties
DAG Theorem
Proof sketch (⇒): If a topological ordering exists, suppose there is a cycle v1→v2→…→vk→v1. Then f(v1) < f(v2) < … < f(vk) < f(v1), a contradiction. So no cycle exists — it must be a DAG.
Proof sketch (⇐): Every DAG has at least one vertex with in-degree 0 (otherwise, following predecessors from any vertex would eventually revisit a vertex, creating a cycle). Remove this vertex, and the remaining graph is still a DAG. Repeat until all vertices are removed. The removal order is a valid topological ordering.
Uniqueness
A topological ordering is unique if and only if there is a Hamiltonian path in the DAG — that is, a directed path that visits every vertex exactly once. Equivalently, at every step of Kahn’s algorithm there is exactly one vertex with in-degree 0. If at any point there are multiple vertices with in-degree 0, multiple valid orderings exist.
Key Observations
- Every DAG has at least one topological ordering.
- A DAG with n vertices can have up to n! topological orderings (e.g., a graph with no edges).
- The first vertex in every topological ordering has in-degree 0.
- The last vertex in every topological ordering has out-degree 0.
Kahn’s Algorithm (BFS-Based)
Kahn’s algorithm (1962) takes the constructive proof above and turns it into an efficient algorithm. It repeatedly finds vertices with in-degree 0 (no remaining prerequisites), removes them from the graph, and adds them to the result.
indegree = [0] * V
for u in 0..V-1:
for v in adj[u]: indegree[v] += 1
queue = [v for v if indegree[v] == 0]
result = []
while queue is not empty:
u = queue.dequeue()
result.append(u)
for v in adj[u]:
indegree[v] -= 1
if indegree[v] == 0: queue.enqueue(v)
if len(result) != V: // cycle detected!
return “Not a DAG”
return result
Step-by-Step Walkthrough
Let’s trace Kahn’s algorithm on our 7-node DAG. We track the in-degree array, queue, and result at each step.
KAHN’S ALGORITHM — BFS LAYER-BY-LAYERInitialize: Compute In-Degrees
Count incoming edges for each node. Node 0 has in-degree 0 (no prerequisites), so it enters the queue.
Result: []
Dequeue 0 — Decrement Neighbors
Remove node 0 from the queue. Decrement in-degree of its neighbors (1 and 2). Both now have in-degree 0, so both enter the queue.
Result: [0]
Dequeue 1 — Unlock 3
Remove node 1. Decrement in-degree of neighbors 3 (1→0) and 4 (2→1). Node 3 now has in-degree 0 and enters the queue. Node 4 still has in-degree 1.
Result: [0, 1]
Dequeue 2 — Unlock 4 and 5
Remove node 2. Decrement neighbors 4 (1→0) and 5 (1→0). Both enter the queue.
Result: [0, 1, 2]
Dequeue 3, 4, 5 — Unlock 6
Remove 3 (in-degree of 6: 3→2), then 4 (2→1), then 5 (1→0). Node 6 finally enters the queue with in-degree 0.
Result: [0, 1, 2, 3, 4, 5]
Dequeue 6 — Done!
Remove node 6. No outgoing edges. Queue is empty. All 7 nodes are in the result.
Final result: [0, 1, 2, 3, 4, 5, 6] ✓DFS-Based Topological Sort
The DFS approach uses a simple observation: in a DFS traversal of a DAG, a node finishes (all its descendants are fully explored) after all nodes reachable from it have finished. If we record nodes in reverse post-order (push to a stack when a node finishes, then reverse), we get a valid topological ordering.
visited = [false] * V
stack = []
for v = 0 to V-1:
if not visited[v]:
dfs(v, adj, visited, stack)
return reverse(stack)
function dfs(u, adj, visited, stack):
visited[u] = true
for v in adj[u]:
if not visited[v]: dfs(v, adj, visited, stack)
stack.push(u) // push on finish
Step-by-Step Walkthrough
Let’s trace DFS topological sort on the same graph, starting from node 0.
DFS-BASED — REVERSE POST-ORDERDFS(0) → DFS(1) → DFS(3) → DFS(6)
Start DFS from node 0. Go deep: 0→1→3→6. Node 6 has no unvisited neighbors — it finishes first and is pushed to the stack.
Backtrack: Finish 3, Then DFS(4)
Node 3 has no more unvisited neighbors — push 3 to stack. Back in DFS(1), next neighbor is 4. DFS(4): neighbor 6 is already visited, so 4 finishes.
Nodes 3, 4 finished
Finish 1, Then DFS(2) → DFS(5)
Node 1 finishes (push 1). Back in DFS(0), next neighbor is 2. DFS(2): neighbor 4 already visited, so explore 5. DFS(5): neighbor 6 already visited, so 5 finishes.
Finish 2, Then Finish 0 — Done!
Node 5 finishes (push 5). Node 2 finishes (push 2). Node 0 finishes (push 0). All nodes are on the finish stack.
Reverse → Topological order: [0, 2, 5, 1, 4, 3, 6] ✓
Both orderings — [0, 1, 2, 3, 4, 5, 6] from Kahn’s and [0, 2, 5, 1, 4, 3, 6] from DFS — are valid. They differ because the algorithms break ties differently.
Kahn’s vs DFS: Comparison
Kahn’s (BFS)
- Uses in-degree array + queue
- Processes nodes layer by layer
- Naturally detects cycles (result.size() ≠ n)
- Easy to modify for lexicographic order (use min-heap)
- Iterative — no recursion stack overflow
DFS-Based
- Uses visited array + recursion stack
- Processes nodes depth-first
- Detects cycles via back edges (needs coloring)
- More natural for problems combining topo sort with DP
- Recursive — may overflow on very deep graphs
| Aspect | Kahn’s (BFS) | DFS |
|---|---|---|
| Time Complexity | O(V + E) | O(V + E) |
| Space Complexity | O(V + E) | O(V + E) |
| Cycle Detection | If result.size() ≠ V | Back edge found during DFS |
| Lexicographic Order | Replace queue with min-heap | Not straightforward |
| Best For | Layer-by-layer processing, lex order | DP on DAGs, SCC algorithms |
Interactive Animation
Step through Kahn’s algorithm at your own pace, or press Play to watch it unfold automatically. The animation shows the queue, in-degree array, and result building up in real time.
Use Step for manual control, Play for auto-advance, or Reset to start over.
Detecting Cycles (Is Topological Sort Possible?)
Topological sort is only defined for DAGs. If the graph has a cycle, no valid ordering exists. Both algorithms can detect this:
Via Kahn’s Algorithm
If the final result contains fewer than V vertices, there is a cycle. This is because nodes in a cycle never reach in-degree 0 — they form a deadlock where each node is waiting for another node in the cycle.
result.size() < V, the graph contains a cycle. The nodes not in the result are exactly the nodes involved in (or reachable only from) cycles.
Via DFS
During DFS, use a three-color scheme: white (unvisited), gray (in current recursion stack), black (finished). If DFS encounters a gray node, that is a back edge, which means a cycle exists.
function dfs(u):
color[u] = GRAY
for v in adj[u]:
if color[v] == GRAY: // back edge → cycle!
return false
if color[v] == WHITE:
if not dfs(v): return false
color[u] = BLACK
stack.push(u)
return true
All Topological Orderings
Sometimes we need to count or enumerate all valid topological orderings of a DAG. This can be done with backtracking: at each step, pick any node with in-degree 0, add it to the current ordering, reduce in-degrees, and recurse. When backtracking, undo the in-degree changes.
if len(result) == V:
print(result) // found one valid ordering
return
for v = 0 to V-1:
if not visited[v] and indeg[v] == 0:
visited[v] = true
result.append(v)
for u in adj[v]: indeg[u] -= 1
allTopoSorts(indeg, result, visited)
// backtrack
result.pop()
visited[v] = false
for u in adj[v]: indeg[u] += 1
For our example graph, the valid topological orderings include:
- [0, 1, 2, 3, 4, 5, 6]
- [0, 1, 2, 3, 5, 4, 6]
- [0, 1, 2, 5, 3, 4, 6]
- [0, 2, 1, 3, 4, 5, 6]
- [0, 2, 1, 3, 5, 4, 6]
- [0, 2, 1, 5, 3, 4, 6]
- [0, 2, 1, 5, 4, 3, 6]
- [0, 1, 3, 2, 4, 5, 6]
- …and more
Lexicographically Smallest Topological Sort
A common competitive programming variant asks for the lexicographically smallest topological ordering. The trick is simple: replace the queue in Kahn’s algorithm with a min-heap (priority queue). At each step, we always pick the smallest-numbered available node.
indegree = [0] * V
for u in 0..V-1:
for v in adj[u]: indegree[v] += 1
min_heap = [v for v if indegree[v] == 0]
result = []
while min_heap is not empty:
u = min_heap.extractMin()
result.append(u)
for v in adj[u]:
indegree[v] -= 1
if indegree[v] == 0: min_heap.insert(v)
return result
For our example, this produces [0, 1, 2, 3, 4, 5, 6] — the lexicographically smallest valid ordering. The time complexity is O((V + E) log V) due to heap operations.
C++ Implementation
Kahn’s Algorithm (BFS)
#include <bits/stdc++.h>
using namespace std;
vector<int> kahnTopoSort(int V, vector<vector<int>>& adj) {
vector<int> indegree(V, 0);
for (int u = 0; u < V; u++)
for (int v : adj[u])
indegree[v]++;
queue<int> q;
for (int i = 0; i < V; i++)
if (indegree[i] == 0)
q.push(i);
vector<int> result;
while (!q.empty()) {
int u = q.front(); q.pop();
result.push_back(u);
for (int v : adj[u]) {
indegree[v]--;
if (indegree[v] == 0)
q.push(v);
}
}
if ((int)result.size() != V) {
// Graph has a cycle — no topological ordering
return {};
}
return result;
}
int main() {
int V = 7;
vector<vector<int>> adj(V);
adj[0] = {1, 2};
adj[1] = {3, 4};
adj[2] = {4, 5};
adj[3] = {6};
adj[4] = {6};
adj[5] = {6};
vector<int> order = kahnTopoSort(V, adj);
if (order.empty()) {
cout << "Cycle detected!" << endl;
} else {
for (int v : order) cout << v << " ";
cout << endl;
}
return 0;
}
// Output: 0 1 2 3 4 5 6
DFS-Based Topological Sort
#include <bits/stdc++.h>
using namespace std;
enum Color { WHITE, GRAY, BLACK };
bool dfs(int u, vector<vector<int>>& adj,
vector<Color>& color, vector<int>& stk) {
color[u] = GRAY;
for (int v : adj[u]) {
if (color[v] == GRAY) return false; // cycle
if (color[v] == WHITE)
if (!dfs(v, adj, color, stk)) return false;
}
color[u] = BLACK;
stk.push_back(u);
return true;
}
vector<int> dfsTopoSort(int V, vector<vector<int>>& adj) {
vector<Color> color(V, WHITE);
vector<int> stk;
for (int i = 0; i < V; i++)
if (color[i] == WHITE)
if (!dfs(i, adj, color, stk))
return {}; // cycle detected
reverse(stk.begin(), stk.end());
return stk;
}
int main() {
int V = 7;
vector<vector<int>> adj(V);
adj[0] = {1, 2};
adj[1] = {3, 4};
adj[2] = {4, 5};
adj[3] = {6};
adj[4] = {6};
adj[5] = {6};
vector<int> order = dfsTopoSort(V, adj);
if (order.empty()) {
cout << "Cycle detected!" << endl;
} else {
for (int v : order) cout << v << " ";
cout << endl;
}
return 0;
}
// Output: 0 2 5 1 4 3 6
Lexicographically Smallest Ordering
#include <bits/stdc++.h>
using namespace std;
vector<int> lexTopoSort(int V, vector<vector<int>>& adj) {
vector<int> indegree(V, 0);
for (int u = 0; u < V; u++)
for (int v : adj[u])
indegree[v]++;
// Min-heap instead of regular queue
priority_queue<int, vector<int>, greater<int>> pq;
for (int i = 0; i < V; i++)
if (indegree[i] == 0)
pq.push(i);
vector<int> result;
while (!pq.empty()) {
int u = pq.top(); pq.pop();
result.push_back(u);
for (int v : adj[u]) {
indegree[v]--;
if (indegree[v] == 0)
pq.push(v);
}
}
if ((int)result.size() != V) return {};
return result;
}
int main() {
int V = 7;
vector<vector<int>> adj(V);
adj[0] = {1, 2};
adj[1] = {3, 4};
adj[2] = {4, 5};
adj[3] = {6};
adj[4] = {6};
adj[5] = {6};
vector<int> order = lexTopoSort(V, adj);
for (int v : order) cout << v << " ";
cout << endl;
return 0;
}
// Output: 0 1 2 3 4 5 6
Applications in Competitive Programming
Longest Path in a DAG
Process nodes in topological order. For each node u, update all neighbors: dp[v] = max(dp[v], dp[u] + w(u,v)). This gives the longest path from any source in O(V+E) — impossible in general graphs but trivial on DAGs.
DP on DAGs
Many DP problems naturally form DAGs. Process states in topological order to ensure all dependencies are computed before a state is evaluated. Examples: number of paths, shortest paths, counting problems.
Task Scheduling with Deadlines
Given tasks with dependencies and deadlines, use topological sort to find a valid execution order, then greedily assign tasks to minimize lateness or maximize completed tasks.
Course Scheduling
Classic problems like “Can you finish all courses?” (cycle detection) and “In what order should you take courses?” (topological sort) appear frequently in interviews and contests.
More Patterns
- Shortest paths in a DAG: Relax edges in topological order — O(V + E), beating Dijkstra for DAGs with negative weights.
- Number of paths: Count paths from source to each node by summing path counts of predecessors in topological order.
- Parallel job scheduling: The minimum number of “rounds” to complete all tasks is the length of the longest path in the DAG (critical path method). Kahn’s naturally gives layers.
- Build order: Determine compilation order for source files with dependencies (like Make, Bazel, Gradle).
Practice Problems
Course Schedule (LeetCode 207)
Determine if it’s possible to finish all courses given prerequisites. This is pure cycle detection in a directed graph — use Kahn’s or DFS.
Course Schedule II (LeetCode 210)
Return a valid ordering of courses. Direct application of topological sort. If a cycle exists, return an empty array.
Alien Dictionary (LeetCode 269)
Given a sorted alien dictionary, deduce the order of characters. Build a DAG from adjacent word comparisons, then topologically sort. Watch for cycles (invalid input) and disconnected components.
Longest Increasing Path in a Matrix (LeetCode 329)
Each cell has edges to neighbors with larger values, forming a DAG. Use DFS + memoization (equivalent to DP in topological order) to find the longest path.
Parallel Courses (LeetCode 1136)
Find the minimum number of semesters to take all courses. This is the longest path in the prerequisite DAG, computable via Kahn’s with level tracking.
Fox and Names (Codeforces 510C)
Determine if a permutation of the alphabet exists such that given names are in lexicographic order. Build a DAG from character constraints and topologically sort.
Topological Sorting (CSES)
Straightforward topological sort on a directed graph. Good for practicing a clean implementation template.
Summary
Let’s recap the key takeaways:
- Topological Sort produces a linear ordering of vertices in a DAG such that every edge goes from earlier to later in the ordering.
- Kahn’s Algorithm (BFS) uses in-degree counting and a queue to peel off source nodes layer by layer. It naturally detects cycles and is easy to modify for lexicographic ordering.
- DFS-based approach records nodes in reverse post-order. It integrates well with DP on DAGs and is the foundation of SCC algorithms.
- Both algorithms run in O(V + E) time and space.
- For lexicographically smallest ordering, use Kahn’s with a min-heap — O((V + E) log V).
- To enumerate all orderings, use backtracking with in-degree tracking (exponential, only for small graphs).