Topological Sort Pattern

1. Course Schedule Problem (Kahn's Algorithm)

Approach Algorithm Time Space
DFS with Cycle Detection White-Gray-Black coloring to detect cycles O(V + E) O(V)
Kahn's Algorithm (BFS) Remove nodes with in-degree 0 iteratively O(V + E) O(V)
DFS Postorder Add to result after exploring all children O(V + E) O(V)
Cycle Check Only Return true/false without generating order O(V + E) O(V)

Example: DFS cycle detection

function canFinish(numCourses, prerequisites) {
    // Build adjacency list
    const graph = Array.from({ length: numCourses }, () => []);
    for (const [course, prereq] of prerequisites) {
        graph[prereq].push(course);
    }
    
    // 0: unvisited (white)
    // 1: visiting (gray - in current path)
    // 2: visited (black - fully processed)
    const state = new Array(numCourses).fill(0);
    
    function hasCycle(node) {
        if (state[node] === 1) return true;  // Cycle!
        if (state[node] === 2) return false; // Already checked
        
        state[node] = 1; // Mark as visiting
        
        for (const neighbor of graph[node]) {
            if (hasCycle(neighbor)) {
                return true;
            }
        }
        
        state[node] = 2; // Mark as visited
        return false;
    }
    
    // Check each node
    for (let i = 0; i < numCourses; i++) {
        if (state[i] === 0 && hasCycle(i)) {
            return false;
        }
    }
    
    return true;
}

// Example: numCourses=2, prerequisites=[[1,0]]
// Course 1 requires course 0
// Result: true (can finish: 0 -> 1)

Example: Kahn's algorithm (BFS)

function canFinishKahn(numCourses, prerequisites) {
    // Build graph and in-degree array
    const graph = Array.from({ length: numCourses }, () => []);
    const inDegree = new Array(numCourses).fill(0);
    
    for (const [course, prereq] of prerequisites) {
        graph[prereq].push(course);
        inDegree[course]++;
    }
    
    // Queue of nodes with no prerequisites
    const queue = [];
    for (let i = 0; i < numCourses; i++) {
        if (inDegree[i] === 0) {
            queue.push(i);
        }
    }
    
    let completed = 0;
    
    while (queue.length > 0) {
        const course = queue.shift();
        completed++;
        
        // Remove this course's edges
        for (const next of graph[course]) {
            inDegree[next]--;
            if (inDegree[next] === 0) {
                queue.push(next);
            }
        }
    }
    
    return completed === numCourses;
}

// If cycle exists, some nodes never reach in-degree 0
// completed < numCourses indicates cycle

Example: Course Schedule II - return topological order

function findOrder(numCourses, prerequisites) {
    const graph = Array.from({ length: numCourses }, () => []);
    const inDegree = new Array(numCourses).fill(0);
    
    for (const [course, prereq] of prerequisites) {
        graph[prereq].push(course);
        inDegree[course]++;
    }
    
    const queue = [];
    for (let i = 0; i < numCourses; i++) {
        if (inDegree[i] === 0) queue.push(i);
    }
    
    const order = [];
    
    while (queue.length > 0) {
        const course = queue.shift();
        order.push(course);
        
        for (const next of graph[course]) {
            inDegree[next]--;
            if (inDegree[next] === 0) {
                queue.push(next);
            }
        }
    }
    
    // If cycle exists, order.length < numCourses
    return order.length === numCourses ? order : [];
}

// Example: numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]
// Result: [0,1,2,3] or [0,2,1,3] (both valid)

2. Alien Dictionary Problem (Character Order)

Step Action Implementation Edge Case
1. Build Graph Compare adjacent words character by character Find first differing char, add edge u→v One word prefix of another (invalid if shorter first)
2. Track In-degrees Count incoming edges for each character Use Map for characters that appear Characters with no edges still need 0 in-degree
3. Topological Sort Kahn's algorithm on character graph Process chars with in-degree 0 Cycle detection (invalid alien language)
4. Validate Result Check if all characters processed order.length === graph.size Empty order indicates cycle or error

Example: Alien dictionary character order

function alienOrder(words) {
    // Build graph and in-degree map
    const graph = new Map();
    const inDegree = new Map();
    
    // Initialize: all characters have in-degree 0
    for (const word of words) {
        for (const char of word) {
            if (!graph.has(char)) {
                graph.set(char, new Set());
                inDegree.set(char, 0);
            }
        }
    }
    
    // Build edges by comparing adjacent words
    for (let i = 0; i < words.length - 1; i++) {
        const word1 = words[i];
        const word2 = words[i + 1];
        const minLen = Math.min(word1.length, word2.length);
        
        // Edge case: word1 is prefix of word2 but longer
        if (word1.length > word2.length && 
            word1.startsWith(word2)) {
            return ""; // Invalid
        }
        
        // Find first different character
        for (let j = 0; j < minLen; j++) {
            if (word1[j] !== word2[j]) {
                const u = word1[j];
                const v = word2[j];
                
                // Add edge u -> v if not exists
                if (!graph.get(u).has(v)) {
                    graph.get(u).add(v);
                    inDegree.set(v, inDegree.get(v) + 1);
                }
                break; // Only first difference matters
            }
        }
    }
    
    // Topological sort using Kahn's algorithm
    const queue = [];
    for (const [char, degree] of inDegree) {
        if (degree === 0) queue.push(char);
    }
    
    let order = '';
    
    while (queue.length > 0) {
        const char = queue.shift();
        order += char;
        
        for (const next of graph.get(char)) {
            inDegree.set(next, inDegree.get(next) - 1);
            if (inDegree.get(next) === 0) {
                queue.push(next);
            }
        }
    }
    
    // Check if valid (no cycle)
    return order.length === graph.size ? order : "";
}

// Example: ["wrt","wrf","er","ett","rftt"]
// Comparisons:
// "wrt" vs "wrf": t -> f
// "wrf" vs "er": w -> e
// "er" vs "ett": r -> t
// "ett" vs "rftt": e -> r
// Result: "wertf"
Note: Alien Dictionary is tricky because you must only compare first differing character between adjacent words. If "abc" comes before "ab", the dictionary is invalid (longer word cannot come first if it's a prefix). Always validate this edge case.

3. Build Order with Dependencies

Variation Difference Implementation Use Case
Single Valid Order Any topological order works Standard Kahn's or DFS Course schedule, build systems
All Valid Orders Generate all possible orderings Backtracking + in-degree tracking Testing, dependency analysis
Lexicographically Smallest Among valid orders, choose smallest Use min-heap instead of queue Deterministic build order
Parallel Execution Group independent tasks by level BFS level-by-level grouping Task scheduling, parallel builds

Example: Lexicographically smallest topological order

function smallestOrder(n, edges) {
    const graph = Array.from({ length: n }, () => []);
    const inDegree = new Array(n).fill(0);
    
    for (const [u, v] of edges) {
        graph[u].push(v);
        inDegree[v]++;
    }
    
    // Use min-heap instead of regular queue
    const minHeap = new MinPriorityQueue({ priority: x => x });
    
    for (let i = 0; i < n; i++) {
        if (inDegree[i] === 0) {
            minHeap.enqueue(i);
        }
    }
    
    const order = [];
    
    while (!minHeap.isEmpty()) {
        const node = minHeap.dequeue().element;
        order.push(node);
        
        for (const next of graph[node]) {
            inDegree[next]--;
            if (inDegree[next] === 0) {
                minHeap.enqueue(next);
            }
        }
    }
    
    return order.length === n ? order : [];
}

// Using min-heap ensures we always pick smallest available node
// Result is lexicographically smallest valid topological order

Example: Group by parallel execution levels

function parallelBuildOrder(n, edges) {
    const graph = Array.from({ length: n }, () => []);
    const inDegree = new Array(n).fill(0);
    
    for (const [u, v] of edges) {
        graph[u].push(v);
        inDegree[v]++;
    }
    
    let queue = [];
    for (let i = 0; i < n; i++) {
        if (inDegree[i] === 0) queue.push(i);
    }
    
    const levels = [];
    
    while (queue.length > 0) {
        const currentLevel = [...queue];
        levels.push(currentLevel);
        
        const nextQueue = [];
        
        for (const node of queue) {
            for (const next of graph[node]) {
                inDegree[next]--;
                if (inDegree[next] === 0) {
                    nextQueue.push(next);
                }
            }
        }
        
        queue = nextQueue;
    }
    
    return levels;
}

// Example: edges = [[0,2],[0,3],[1,3],[1,4]]
// Result: [[0,1], [2,3,4]]
// Level 0: tasks 0,1 can run in parallel
// Level 1: tasks 2,3,4 can run in parallel after level 0

4. All Topological Orderings of DAG

Approach Method Time Space
Backtracking Try each node with in-degree 0, recurse O(V! * E) O(V)
State Tracking Maintain in-degree, used set, current path Exponential O(V) depth
Pruning Only try nodes with current in-degree 0 Reduces branches Critical optimization
Count Only Count orderings instead of generating Same complexity More efficient

Example: Generate all topological orderings

function allTopologicalOrders(graph) {
    const n = graph.length;
    const inDegree = new Array(n).fill(0);
    
    // Calculate in-degrees
    for (let u = 0; u < n; u++) {
        for (const v of graph[u]) {
            inDegree[v]++;
        }
    }
    
    const result = [];
    const current = [];
    const visited = new Array(n).fill(false);
    
    function backtrack() {
        // Base case: all nodes processed
        if (current.length === n) {
            result.push([...current]);
            return;
        }
        
        // Try each node with current in-degree 0
        for (let i = 0; i < n; i++) {
            if (!visited[i] && inDegree[i] === 0) {
                // Choose this node
                visited[i] = true;
                current.push(i);
                
                // Reduce in-degree of neighbors
                for (const neighbor of graph[i]) {
                    inDegree[neighbor]--;
                }
                
                // Recurse
                backtrack();
                
                // Backtrack: restore state
                for (const neighbor of graph[i]) {
                    inDegree[neighbor]++;
                }
                current.pop();
                visited[i] = false;
            }
        }
    }
    
    backtrack();
    return result;
}

// Example: graph = [[1], [2], []]
// Nodes: 0 -> 1 -> 2
// Result: [[0,1,2]] (only one valid order)

// Example: graph = [[2], [2], []]
// Nodes: 0 -> 2, 1 -> 2
// Result: [[0,1,2], [1,0,2]] (two valid orders)
Warning: Generating all topological orderings has factorial time complexity in worst case (when graph is completely disconnected). Use only for small graphs (n ≤ 10). For larger graphs, generate one order or count orderings using DP.

5. Sequence Reconstruction Verification

Challenge Condition Check Complexity
Uniqueness Only one valid topological order At most one node with in-degree 0 at each step O(V + E)
Completeness All consecutive pairs covered Every edge in order exists in sequences Check all pairs
Cycle Check No cycles in dependency graph Topological sort completes Standard check
Coverage All nodes appear in sequences Union of sequences == all nodes O(total elements)

Example: Verify sequence reconstruction

function sequenceReconstruction(nums, sequences) {
    const n = nums.length;
    const graph = new Map();
    const inDegree = new Map();
    
    // Initialize graph for all numbers
    for (const num of nums) {
        graph.set(num, new Set());
        inDegree.set(num, 0);
    }
    
    // Build graph from sequences
    for (const seq of sequences) {
        for (let i = 0; i < seq.length - 1; i++) {
            const u = seq[i];
            const v = seq[i + 1];
            
            // Validate numbers are in range
            if (!graph.has(u) || !graph.has(v)) {
                return false;
            }
            
            // Add edge if not exists
            if (!graph.get(u).has(v)) {
                graph.get(u).add(v);
                inDegree.set(v, inDegree.get(v) + 1);
            }
        }
    }
    
    // Topological sort with uniqueness check
    const queue = [];
    for (const [num, degree] of inDegree) {
        if (degree === 0) queue.push(num);
    }
    
    const order = [];
    
    while (queue.length > 0) {
        // CRITICAL: Must have exactly one node with in-degree 0
        if (queue.length > 1) {
            return false; // Not unique
        }
        
        const num = queue.shift();
        order.push(num);
        
        for (const next of graph.get(num)) {
            inDegree.set(next, inDegree.get(next) - 1);
            if (inDegree.get(next) === 0) {
                queue.push(next);
            }
        }
    }
    
    // Check if we got complete order matching nums
    if (order.length !== n) return false;
    
    for (let i = 0; i < n; i++) {
        if (order[i] !== nums[i]) return false;
    }
    
    return true;
}

// Example: nums = [1,2,3], sequences = [[1,2],[1,3]]
// Result: false (both [1,2,3] and [1,3,2] are valid)

// Example: nums = [1,2,3], sequences = [[1,2],[1,3],[2,3]]
// Result: true (only [1,2,3] is valid)

// Key: queue.length > 1 means multiple valid orderings exist

Example: Alternative - check pairs coverage

function sequenceReconstructionAlt(nums, sequences) {
    const n = nums.length;
    const pairs = new Set();
    
    // Collect all consecutive pairs from sequences
    for (const seq of sequences) {
        for (let i = 0; i < seq.length - 1; i++) {
            pairs.add(`${seq[i]},${seq[i+1]}`);
        }
    }
    
    // Check if all consecutive pairs in nums are covered
    for (let i = 0; i < n - 1; i++) {
        if (!pairs.has(`${nums[i]},${nums[i+1]}`)) {
            return false; // Missing edge
        }
    }
    
    // Build graph and check uniqueness via topological sort
    const graph = new Map();
    const inDegree = new Map();
    
    for (const num of nums) {
        graph.set(num, new Set());
        inDegree.set(num, 0);
    }
    
    for (const seq of sequences) {
        for (let i = 0; i < seq.length - 1; i++) {
            const u = seq[i];
            const v = seq[i + 1];
            
            if (!graph.has(u) || !graph.has(v)) return false;
            
            if (!graph.get(u).has(v)) {
                graph.get(u).add(v);
                inDegree.set(v, inDegree.get(v) + 1);
            }
        }
    }
    
    // Verify uniqueness (same as before)
    let queue = [];
    for (const [num, degree] of inDegree) {
        if (degree === 0) queue.push(num);
    }
    
    while (queue.length > 0) {
        if (queue.length > 1) return false;
        
        const num = queue.shift();
        for (const next of graph.get(num)) {
            inDegree.set(next, inDegree.get(next) - 1);
            if (inDegree.get(next) === 0) queue.push(next);
        }
    }
    
    return true;
}

// Two conditions must hold:
// 1. All pairs in nums exist in sequences (coverage)
// 2. Only one topological order exists (uniqueness)

Summary: Topological Sort Patterns

  • Two Main Algorithms: DFS postorder (add to result after exploring children) and Kahn's BFS (process nodes with in-degree 0)
  • DFS Cycle Detection: Three states: unvisited (white), visiting (gray - in current path), visited (black) - cycle if revisit gray node
  • Kahn's Algorithm: Initialize queue with in-degree 0 nodes, process each, decrement neighbors' in-degree, add to queue when 0
  • Cycle Detection Kahn's: If processed nodes < total nodes, cycle exists (some nodes never reach in-degree 0)
  • Course Schedule: Can finish if no cycles - use either DFS cycle detection or Kahn's with count check
  • Alien Dictionary: Compare adjacent words for first difference, add edge for character ordering, validate prefix edge case
  • Prefix Edge Case: If word1.startsWith(word2) but word1.length > word2.length, dictionary is invalid
  • Lexicographically Smallest: Use min-heap instead of queue in Kahn's algorithm to always pick smallest available node
  • Parallel Execution: Group by BFS levels - all nodes in same level can execute in parallel
  • All Orderings: Backtracking with in-degree tracking - try each in-degree 0 node, recurse, restore state - O(V!) complexity
  • Sequence Reconstruction: Verify uniqueness (queue.length ≤ 1 at each step) and completeness (all pairs in target exist in sequences)
  • Uniqueness Check: During Kahn's, if queue.length > 1, multiple valid orderings exist - order is not unique
  • Applications: Build systems, dependency resolution, task scheduling, compiler optimization, package managers