2Brute2Best
πŸ”₯0

Topological Sort Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide

Order tasks so every prerequisite comes first. Repeatedly take tasks that have no remaining prerequisites.

4 sub-patterns Β· 15 questions

Remember this Β· Topological Sort
Trigger
Prerequisites, dependencies, build order, "can all courses be finished?", ordering letters from sorted words.
Think
Is this a directed graph where some things must happen before others?
Key question
Which nodes currently have in-degree 0?
A

What is this pattern?

A topological order lists the nodes of a directed graph so that every edge points forward: if u β†’ v means "u must come before v", then u appears earlier in the list. Course plans, build steps and task schedules are all this question.

The idea behind Kahn's algorithm: a node with in-degree 0 (no arrow points into it) has no prerequisites left, so it is safe to place now. Place it, delete its outgoing edges, and some other nodes may drop to in-degree 0. Repeat with a queue. Each node and edge is handled once: O(V + E).

An order exists only if there is no cycle: in a cycle every node waits for another one, so none of them ever reaches in-degree 0. That makes the same algorithm a cycle detector: if fewer than V nodes get placed, there is a cycle. The DFS view gives the same results: finishing order reversed is a topological order, and an edge back to a node still on the DFS path is a cycle. Memoised DFS over a DAG also computes DP along the dependencies (longest path, earliest finish time).

Getting dressed. Socks before shoes, underwear before trousers, trousers before shoes, shirt before tie. At any moment you can put on any item that has nothing left that must go on first (in-degree 0). Putting on socks frees the shoes from one of their two requirements; once the trousers are on too, the shoes are free. If the rules ever said "shoes before socks" as well, you'd stand there forever: that's a cycle.

B

See it move

Blue = in the queue (in-degree 0), purple = popped now, green = placed. in = unplaced prerequisites.

Kahn's algorithm: place what has no prerequisites leftn = 6, edges = [[0,2],[1,2],[1,3],[2,4],[3,4],[4,5],[3,5]]step 1/8
prerequisites (u β†’ v: u first)
0in=01in=02in=23in=14in=25in=2
queue
0
1
0
1
β–²front
order
Start: in-degree 0 β†’ 0, 1

in = how many unplaced prerequisites a node still has. Nodes 0 and 1 have none, so they can go first: both start in the queue. Blue = in the queue, purple = popped now, green = placed.

Blue = grey (on the current path), green = black (finished), red = the edge that closes a cycle, orange = an edge to a finished node (safe).

Cycle check: three-colour DFSn = 6, edges = [[0,1],[0,2],[1,3],[2,3],[2,4],[4,5],[5,2]]step 1/12
directed graph
0white1white2white3white4white5white
path
All nodes white

white = not visited, grey = on the current DFS path (blue), black = finished, every path from it checked (green). A cycle shows up as an edge into a grey node.

C

How do I recognise it?

  • "Prerequisites", "depends on", "must be done before", "build order", "course schedule".

  • "Can all tasks be finished?" β†’ is the dependency graph free of cycles?

  • "Return any valid order" / "return all valid orders" of tasks.

  • "Minimum number of semesters / time to finish" when tasks run in parallel β†’ Kahn level by level, or the longest path in the DAG.

  • Sorted words in an unknown alphabet β†’ the letter order is hidden in the first difference of adjacent words.

  • "Safe" / "eventually terminal" nodes, or anything that must not lead into a loop.

Looks like Topological Sort, but isn't

Undirected graph: "is there a cycle?", "connected?"
β†’ Union-Find or Graph BFS / DFS

In-degree and "before" only make sense when edges have a direction. An undirected cycle check uses Union-Find (an edge inside one group) or DFS with a parent check.

Shortest path from a start node
β†’ Graph BFS or Dijkstra

Topological sort gives an order of all nodes, not distances from one start. (On a DAG you can compute shortest/longest paths in topological order, but that's the DAG DP use, not plain sorting.)

Order intervals or tasks by start time / deadline
β†’ Intervals or Greedy

Sorting by a number needs no graph. Topological sort is for pairwise rules ("a before b") that don't come from a single key.

D

How would I discover it from scratch?

Don't memorise β€œuse topological sort”. Ask these questions in order β€” the pattern falls out.

Decide it once and write it down: edge u β†’ v means u comes first. Course Schedule gives [a, b] meaning "to take a you must first take b", so the edge is b β†’ a. Getting this backwards gives the order reversed, which fails for every edge.

E

Sub-patterns

Same core idea, different rule for β€œvalid”. Learn them in this order.

F

Don't mix these up

Kahn (BFS on in-degrees) vs DFS finishing order

Both give a valid order in O(V + E). Kahn detects a cycle by counting and naturally gives levels (semesters). DFS fits when you also compute a value per node (memo). Pick Kahn when unsure: no recursion depth limits.

Directed cycle (three colours) vs Undirected cycle (visited + parent)

In a directed graph, a visited node reached again may just be a second road to it (0β†’1β†’3, 0β†’2β†’3). Only a node still on the current path closes a cycle. In an undirected graph any visited node other than the parent closes one.

Edge `[a, b]` = a β†’ b vs Edge `[a, b]` = b β†’ a

Read the statement: "to take a you must first take b" makes b the prerequisite, so the arrow is b β†’ a. A reversed graph gives the order backwards and no error message.

Any valid order vs The only valid order

Kahn with a queue that ever holds two nodes has a free choice there, so more than one order exists. Uniqueness = the queue size is exactly 1 at every step.

G

Code templates (C++)

Kahn's algorithm (BFS on in-degrees)

Count in-degrees, queue every 0, pop β†’ append β†’ lower successors. Returns {} when fewer than n nodes are placed (a cycle). Parallel Courses counts the levels of this loop instead of the order.

kahn-template.cpp
// Kahn's algorithm: repeatedly take a node nothing points to any more.
vector<int> topoSort(int n, const vector<vector<int>>& edges) {   // edge {u, v} = u must come before v
    vector<vector<int>> adj(n);
    vector<int> indeg(n, 0);
    for (auto& e : edges) { adj[e[0]].push_back(e[1]); indeg[e[1]]++; }
    queue<int> q;
    for (int v = 0; v < n; v++) if (indeg[v] == 0) q.push(v);
    vector<int> order;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);                       // everything before u is already placed
        for (int v : adj[u])
            if (--indeg[v] == 0) q.push(v);       // v's last prerequisite just got placed
    }
    if ((int)order.size() < n) return {};         // some nodes never reached in-degree 0: a cycle
    return order;
}

Cycle check: three-colour DFS

Grey = on the current path, black = finished. An edge into grey is a back edge, a cycle; an edge into black is just another road to a node already checked.

cycle-dfs-template.cpp
// Directed cycle check with three colours: 0 = not seen, 1 = on the current path, 2 = finished.
bool dfsCycle(int u, const vector<vector<int>>& adj, vector<int>& color) {
    color[u] = 1;
    for (int v : adj[u]) {
        if (color[v] == 1) return true;           // back edge to the current path: a cycle
        if (color[v] == 0 && dfsCycle(v, adj, color)) return true;
    }
    color[u] = 2;                                 // every path from u is cycle-free
    return false;
}

bool hasCycle(int n, const vector<vector<int>>& edges) {
    vector<vector<int>> adj(n);
    for (auto& e : edges) adj[e[0]].push_back(e[1]);
    vector<int> color(n, 0);
    for (int u = 0; u < n; u++)
        if (color[u] == 0 && dfsCycle(u, adj, color)) return true;
    return false;
}

DAG DP with a memoised DFS

A node's answer is built from its successors' answers, each solved once. Longest Increasing Path, Loud and Rich and Parallel Courses III all have this shape; the memo also acts as the visited set.

dag-dp-template.cpp
// DP on a DAG with a memoised DFS: a node's answer is built from its successors' answers.
// Here: the longest path (in nodes) that starts at each node.
int longestFrom(int u, const vector<vector<int>>& adj, vector<int>& memo) {
    if (memo[u] != 0) return memo[u];             // already solved: each node is solved once
    int best = 1;
    for (int v : adj[u]) best = max(best, 1 + longestFrom(v, adj, memo));
    return memo[u] = best;
}

int longestPath(int n, const vector<vector<int>>& edges) {   // the graph must be a DAG
    vector<vector<int>> adj(n);
    for (auto& e : edges) adj[e[0]].push_back(e[1]);
    vector<int> memo(n, 0);
    int ans = 0;
    for (int u = 0; u < n; u++) ans = max(ans, longestFrom(u, adj, memo));
    return ans;
}

Build the graph from comparisons (Alien Dictionary)

One edge per adjacent pair of words, from their first difference. A longer word before its own prefix is impossible. Then Kahn over the letters that appear.

build-graph-template.cpp
// Alien dictionary: each adjacent pair of words gives at most one edge, from their first difference.
string alienOrder(const vector<string>& words) {
    vector<vector<bool>> edge(26, vector<bool>(26, false));
    vector<int> indeg(26, 0);
    vector<bool> used(26, false);
    for (auto& w : words) for (char ch : w) used[ch - 'a'] = true;
    for (int i = 0; i + 1 < (int)words.size(); i++) {
        const string &a = words[i], &b = words[i + 1];
        int j = 0;
        while (j < (int)min(a.size(), b.size()) && a[j] == b[j]) j++;
        if (j == (int)min(a.size(), b.size())) {
            if (a.size() > b.size()) return "";   // "abc" before "ab": no order works
            continue;                             // a prefix tells us nothing
        }
        int u = a[j] - 'a', v = b[j] - 'a';       // only the FIRST difference matters
        if (!edge[u][v]) { edge[u][v] = true; indeg[v]++; }
    }
    queue<int> q;                                 // then plain Kahn over the used letters
    for (int c = 0; c < 26; c++) if (used[c] && indeg[c] == 0) q.push(c);
    string order;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order += char('a' + u);
        for (int v = 0; v < 26; v++)
            if (edge[u][v] && --indeg[v] == 0) q.push(v);
    }
    return order.size() == (size_t)count(used.begin(), used.end(), true) ? order : "";
}