← All Posts
DSA Series · Graphs · Topological Sort

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).

Formal definition: A topological ordering of a DAG G = (V, E) is a bijection f : V → {1, 2, …, |V|} such that for every edge (u, v) ∈ E, we have f(u) < f(v).

Consider this DAG with 7 nodes representing course prerequisites:

0 1 2 3 4 5 6

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

Formal Properties

DAG Theorem

Theorem: A directed graph has a topological ordering if and only if it is a DAG.

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

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.

function kahn(V, adj):
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-LAYER

Initialize: Compute In-Degrees

Count incoming edges for each node. Node 0 has in-degree 0 (no prerequisites), so it enters the queue.

0 1 2 3 4 5 6
In-degree: [0:0, 1:1, 2:1, 3:1, 4:2, 5:1, 6:3]

Queue: [0]

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.

0 1 2 3 4 5 6
In-degree: [0:—, 1:0, 2:0, 3:1, 4:2, 5:1, 6:3]

Queue: [1, 2]

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.

In-degree: [0:—, 1:—, 2:0, 3:0, 4:1, 5:1, 6:3]

Queue: [2, 3]

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.

0 1 2 3 4 5 6
In-degree: [0:—, 1:—, 2:—, 3:0, 4:0, 5:0, 6:3]

Queue: [3, 4, 5]

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.

In-degree: [0:—, 1:—, 2:—, 3:—, 4:—, 5:—, 6:0]

Queue: [6]

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.

0 1 2 3 4 5 6 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.

function topoSort_DFS(V, adj):
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-ORDER

DFS(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.

0 1 2 3 4 5 6
Call stack: [dfs(0), dfs(1), dfs(3), dfs(6)]

Finish stack: [6]  (6 finishes first)

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.

Call stack: [dfs(0), dfs(1)]

Finish stack: [6, 3, 4]

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.

0 1 2 3 4 5 6
Call stack: [dfs(0), dfs(2), dfs(5)]

Finish stack: [6, 3, 4, 1, 5]

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.

0 1 2 3 4 5 6
Finish stack (bottom to top): [6, 3, 4, 1, 5, 2, 0]

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
AspectKahn’s (BFS)DFS
Time ComplexityO(V + E)O(V + E)
Space ComplexityO(V + E)O(V + E)
Cycle DetectionIf result.size() ≠ VBack edge found during DFS
Lexicographic OrderReplace queue with min-heapNot straightforward
Best ForLayer-by-layer processing, lex orderDP 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.

0 1 2 3 4 5 6

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.

Cycle detection with Kahn’s: Run Kahn’s algorithm. If 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.

// DFS with 3-color cycle detection
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.

function allTopoSorts(indeg, result, visited):
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:

Complexity warning: The number of topological orderings can be exponential. For a graph with no edges, it’s n!. This backtracking approach is O(V! · V) in the worst case. Only use it when V is small (typically V ≤ 20).

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.

function lexTopoSort(V, adj):
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.

Important: The DFS approach cannot easily produce the lexicographically smallest topological ordering. Even if you visit neighbors in sorted order during DFS, the reverse post-order may not be lexicographically smallest. Kahn’s with a min-heap is the correct approach.

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
Complexity note: Kahn’s and DFS both run in O(V + E). The lexicographic variant is O((V + E) log V) due to priority queue operations. All use O(V + E) space.

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

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:

  1. Topological Sort produces a linear ordering of vertices in a DAG such that every edge goes from earlier to later in the ordering.
  2. 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.
  3. DFS-based approach records nodes in reverse post-order. It integrates well with DP on DAGs and is the foundation of SCC algorithms.
  4. Both algorithms run in O(V + E) time and space.
  5. For lexicographically smallest ordering, use Kahn’s with a min-heap — O((V + E) log V).
  6. To enumerate all orderings, use backtracking with in-degree tracking (exponential, only for small graphs).
Practice tip: Master both Kahn’s and DFS approaches. In contests, Kahn’s is often faster to code and debug. For problems combining topological sort with DP (e.g., longest path), the DFS approach may feel more natural. Always check for cycles!