Topological Sort

GraphsPriority 4 of 5 — High value — a gap here costs you roundsHigh value Updated Nov 12, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

Scope — Ordering a DAG — Kahn’s BFS, DFS post-order, cycle detection, and the scheduling problems built on them. See also: topology_sorting_examples.md — the eight worked problems behind these templates; diff_toposort_quickunion.md — toposort vs union-find — which tool for which problem; union_find.md — undirected connectivity; graph.md — general graph material.

LeetCode Problem Lists

Overview

Topological sorting is a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that for every directed edge (u, v), vertex u comes before v in the ordering.

Key Characteristics

  • DAG Only: Works only on Directed Acyclic Graphs
  • Multiple Valid Orders: Many valid topological orders may exist
  • Dependency Resolution: Solves problems with prerequisite/dependency relationships
  • Applications: Task scheduling, build systems, course planning, dependency resolution

Complexity Analysis

Approach Time Complexity Space Complexity Use Case
DFS (three-colour post-order) O(V + E) O(V) General purpose, cycle detection
BFS (In-degree) O(V + E) O(V) Finding all orderings, level-by-level
Tree Centroid Finding O(V + E) O(V) Undirected trees, find center/minimize height
All Topological Sorts O(V! × (V + E)) O(V) Small graphs, all permutations

References

Problem Categories

1. Course Scheduling

Problems involving prerequisite relationships and course ordering.

  • Pattern: Build dependency graph, check for cycles, find valid ordering
  • Key Problems: LC 207, 210, 630, 1462

2. Task Scheduling

Problems involving task dependencies and parallel execution.

  • Pattern: Find minimum time, parallel processing levels
  • Key Problems: LC 1136, 2050, 1857

3. Lexicographical Ordering

Problems requiring smallest/largest lexicographical topological order.

  • Pattern: Priority queue for ordering, alien dictionary
  • Key Problems: LC 269, 953, 1203

4. Build Order & Dependencies

Problems involving build systems and package dependencies.

  • Pattern: Detect cycles, find build order, handle groups
  • Key Problems: LC 444, 802, 851

5. Graph Layering

Problems involving level-by-level processing in DAGs.

  • Pattern: BFS with levels, longest path in DAG
  • Key Problems: LC 2192, 2115, 1857

6. Cycle Detection & Safe States

Problems focused on detecting cycles and finding safe nodes.

  • Pattern: Three-color DFS, safe states identification
  • Key Problems: LC 802, 207, 1059

7. Connected Components (Union Find / DFS)

Problems involving finding connected components in undirected graphs.

  • Pattern: Union Find with path compression, DFS/BFS traversal to count components
  • Key Problems: LC 547, 200, 323, 684

8. Tree Centroid Finding

Problems involving finding the center/centroid of undirected trees.

  • Pattern: Leaf trimming layer by layer — Kahn’s peeling seeded on degree == 1 instead of in_degree == 0
  • Key Problems: LC 310, 1245, 2603

Core Templates

Template 1: BFS (Kahn’s Algorithm) Priority 5 of 5 — Must know — expect it in almost every loop

python
def topologicalSort_BFS(numNodes, edges):
    """
    BFS-based topological sort using in-degree tracking.
    Time: O(V + E), Space: O(V)
    """
    from collections import defaultdict, deque
    
    # Build graph and calculate in-degrees
    graph = defaultdict(list)
    in_degree = [0] * numNodes
    
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
    
    # Initialize queue with nodes having no dependencies
    queue = deque([i for i in range(numNodes) if in_degree[i] == 0])
    result = []
    
    while queue:
        node = queue.popleft()
        result.append(node)
        
        # Process neighbors
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    
    # Check for cycles
    return result if len(result) == numNodes else []
java
// Java version
public List<Integer> topologicalSort_BFS(int numNodes, int[][] edges) {
    Map<Integer, List<Integer>> graph = new HashMap<>();
    int[] inDegree = new int[numNodes];
    
    // Build graph
    for (int i = 0; i < numNodes; i++) {
        graph.put(i, new ArrayList<>());
    }
    
    for (int[] edge : edges) {
        graph.get(edge[0]).add(edge[1]);
        inDegree[edge[1]]++;
    }
    
    // BFS
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < numNodes; i++) {
        if (inDegree[i] == 0) queue.offer(i);
    }
    
    List<Integer> result = new ArrayList<>();
    while (!queue.isEmpty()) {
        int node = queue.poll();
        result.add(node);
        
        for (int neighbor : graph.get(node)) {
            if (--inDegree[neighbor] == 0) {
                queue.offer(neighbor);
            }
        }
    }
    
    return result.size() == numNodes ? result : new ArrayList<>();
}

Template 2: DFS (Three-Color) Priority 4 of 5 — High value — a gap here costs you rounds

python
def topologicalSort_DFS(numNodes, edges):
    """
    DFS-based topological sort with three-color marking.
    Time: O(V + E), Space: O(V)
    """
    from collections import defaultdict
    
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
    
    # 0: white (unvisited), 1: gray (visiting), 2: black (visited)
    color = [0] * numNodes
    result = []
    has_cycle = False
    
    def dfs(node):
        nonlocal has_cycle
        if color[node] == 1:  # Gray = cycle detected
            has_cycle = True
            return
        if color[node] == 2:  # Black = already processed
            return
        
        color[node] = 1  # Mark as visiting
        for neighbor in graph[node]:
            dfs(neighbor)
        color[node] = 2  # Mark as visited
        result.append(node)  # Add to result in reverse order
    
    for i in range(numNodes):
        if color[i] == 0:
            dfs(i)
    
    return [] if has_cycle else result[::-1]
java
// Java version
public List<Integer> topologicalSort_DFS(int numNodes, int[][] edges) {
    Map<Integer, List<Integer>> graph = new HashMap<>();
    for (int i = 0; i < numNodes; i++) {
        graph.put(i, new ArrayList<>());
    }
    for (int[] edge : edges) {
        graph.get(edge[0]).add(edge[1]);
    }
    
    int[] color = new int[numNodes]; // 0: white, 1: gray, 2: black
    List<Integer> result = new ArrayList<>();
    boolean[] hasCycle = {false};
    
    for (int i = 0; i < numNodes; i++) {
        if (color[i] == 0) {
            dfs(i, graph, color, result, hasCycle);
        }
    }
    
    if (hasCycle[0]) return new ArrayList<>();
    Collections.reverse(result);
    return result;
}

private void dfs(int node, Map<Integer, List<Integer>> graph, 
                 int[] color, List<Integer> result, boolean[] hasCycle) {
    if (color[node] == 1) {
        hasCycle[0] = true;
        return;
    }
    if (color[node] == 2) return;
    
    color[node] = 1;
    for (int neighbor : graph.get(node)) {
        dfs(neighbor, graph, color, result, hasCycle);
    }
    color[node] = 2;
    result.add(node);
}

Template 3: DFS (Stack-based)

python
# V0
# IDEA : implement topologicalSortUtil, topologicalSort, and addEdge methods
# step 1) maintain a stack, save "ordering" nodes in it (and return in final step)
# step 2) init visited as [False]*self.V  (all nodes are NOT visited yet)
# step 3) iterate over all vertices in graph, if not visited, then run topologicalSortUtil
# step 4) return result (stack)
from collections import defaultdict
class Graph:
    def __init__(self, vertices):
        self.graph = defaultdict(list)
        self.V = vertices

    # for build graph
    def addEdge(self, u, v):
        self.graph[u].append(v)

    def topologicalSortUtil(self, v, visited, stack):
        visited[v] = True
        # visit every successor of v first ...
        for k in self.graph[v]:
            if not visited[k]:
                self.topologicalSortUtil(k, visited, stack)
        # ... so v is appended only after everything that must come AFTER it (post-order)
        stack.append(v)

    def topologicalSort(self):
        visited = [False] * self.V
        stack = []
        # start a DFS from every unvisited vertex, so disconnected parts are covered
        for v in range(self.V):
            if not visited[v]:
                self.topologicalSortUtil(v, visited, stack)
        # post-order lists each node after its successors; reverse it once
        return stack[::-1]

### TEST
{"A": 0, "B":1, "C":2, "D": 3}
v = 4
g = Graph(v)
g.addEdge(0, 1)
g.addEdge(0, 2)
g.addEdge(2, 3)
g.addEdge(3, 1)

print (g.graph)

# -> [0, 2, 3, 1]: every edge u -> v has u before v
r = g.topologicalSort()
print (r)

Template 4: Lexicographical Order

python
def topologicalSort_Lexicographical(numNodes, edges):
    """
    Find smallest lexicographical topological order.
    Time: O((V + E) log V), Space: O(V)
    """
    from collections import defaultdict
    import heapq
    
    graph = defaultdict(list)
    in_degree = [0] * numNodes
    
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
    
    # Use min-heap for smallest lexicographical order
    heap = [i for i in range(numNodes) if in_degree[i] == 0]
    heapq.heapify(heap)
    result = []
    
    while heap:
        node = heapq.heappop(heap)
        result.append(node)
        
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                heapq.heappush(heap, neighbor)
    
    return result if len(result) == numNodes else []
java
// Java version
public List<Integer> topologicalSort_Lexicographical(int numNodes, int[][] edges) {
    Map<Integer, List<Integer>> graph = new HashMap<>();
    int[] inDegree = new int[numNodes];
    
    for (int i = 0; i < numNodes; i++) {
        graph.put(i, new ArrayList<>());
    }
    
    for (int[] edge : edges) {
        graph.get(edge[0]).add(edge[1]);
        inDegree[edge[1]]++;
    }
    
    // Min-heap for lexicographical order
    PriorityQueue<Integer> pq = new PriorityQueue<>();
    for (int i = 0; i < numNodes; i++) {
        if (inDegree[i] == 0) pq.offer(i);
    }
    
    List<Integer> result = new ArrayList<>();
    while (!pq.isEmpty()) {
        int node = pq.poll();
        result.add(node);
        
        for (int neighbor : graph.get(node)) {
            if (--inDegree[neighbor] == 0) {
                pq.offer(neighbor);
            }
        }
    }
    
    return result.size() == numNodes ? result : new ArrayList<>();
}

Template 5: All Topological Orders

python
def allTopologicalSorts(graph, in_degree, path, result, visited):
    """
    Find all possible topological orderings.
    Time: O(V! × (V + E)), Space: O(V)
    """
    if len(path) == len(graph):
        result.append(path[:])
        return
    
    for node in range(len(graph)):
        if in_degree[node] == 0 and not visited[node]:
            # Choose node
            visited[node] = True
            path.append(node)
            
            # Update in-degrees
            for neighbor in graph[node]:
                in_degree[neighbor] -= 1
            
            # Recurse
            allTopologicalSorts(graph, in_degree, path, result, visited)
            
            # Backtrack
            for neighbor in graph[node]:
                in_degree[neighbor] += 1
            path.pop()
            visited[node] = False

# Usage
def findAllOrders(numNodes, edges):
    from collections import defaultdict
    
    graph = defaultdict(list)
    in_degree = [0] * numNodes
    
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
    
    result = []
    visited = [False] * numNodes
    allTopologicalSorts(graph, in_degree, [], result, visited)
    return result

Template 6: Parallel Task Scheduling

python
def parallelTaskScheduling(numTasks, edges, times):
    """
    Find minimum time to complete all tasks with parallel execution.
    Time: O(V + E), Space: O(V)
    """
    from collections import defaultdict, deque
    
    graph = defaultdict(list)
    in_degree = [0] * numTasks
    
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
    
    # Track completion time for each task
    completion_time = [0] * numTasks
    queue = deque()
    
    # Initialize with tasks having no dependencies
    for i in range(numTasks):
        if in_degree[i] == 0:
            queue.append(i)
            completion_time[i] = times[i]
    
    while queue:
        task = queue.popleft()
        
        for next_task in graph[task]:
            # Update completion time
            completion_time[next_task] = max(
                completion_time[next_task],
                completion_time[task] + times[next_task]
            )
            
            in_degree[next_task] -= 1
            if in_degree[next_task] == 0:
                queue.append(next_task)
    
    return max(completion_time)
java
// Java version
public int parallelTaskScheduling(int numTasks, int[][] edges, int[] times) {
    Map<Integer, List<Integer>> graph = new HashMap<>();
    int[] inDegree = new int[numTasks];
    int[] completionTime = new int[numTasks];
    
    for (int i = 0; i < numTasks; i++) {
        graph.put(i, new ArrayList<>());
    }
    
    for (int[] edge : edges) {
        graph.get(edge[0]).add(edge[1]);
        inDegree[edge[1]]++;
    }
    
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < numTasks; i++) {
        if (inDegree[i] == 0) {
            queue.offer(i);
            completionTime[i] = times[i];
        }
    }
    
    while (!queue.isEmpty()) {
        int task = queue.poll();
        
        for (int nextTask : graph.get(task)) {
            completionTime[nextTask] = Math.max(
                completionTime[nextTask],
                completionTime[task] + times[nextTask]
            );
            
            if (--inDegree[nextTask] == 0) {
                queue.offer(nextTask);
            }
        }
    }
    
    int maxTime = 0;
    for (int time : completionTime) {
        maxTime = Math.max(maxTime, time);
    }
    return maxTime;
}

Template 7: Tree Centroid Finding (Leaf Trimming for Undirected Trees) Priority 4 of 5 — High value — a gap here costs you rounds

LC Pattern — three signals in the statement, all present in LC 310:

Signal What it tells you
undirected, n nodes and n - 1 edges, connected it is a tree — no cycles, so no cycle handling and no visited set
“you can choose any node as the root”, “minimum height” you are asked for a node set, not an ordering
the answer is a list, and “at most 2” is hinted the survivors of the peel are the answer

LC 310 = find the tree center = keep removing leaves.

Core Idea — the root that minimises height is the midpoint of the diameter. Instead of trying every root (an O(n^2) BFS-from-each-node, V1'' in the solution file), peel inward: a leaf is always the worst possible root, so discard the entire leaf layer at once and repeat. What survives is equidistant from both ends of the diameter — the center.

text
        0                 layer 1 leaves: 2, 3, 5        remaining = 6 - 3 = 3
        |                 remove them
        1              ────────────────────────────────
      / | \                 0 - 1 - 4
     2  3  4             layer 2 leaves: 0, 4            remaining = 3 - 2 = 1
           |             remove them
           5           ────────────────────────────────
                              1                          remaining = 1 → stop, answer [1]

Why 1 or 2 survivors, never 3 — the survivors are the midpoint(s) of the diameter path, and a path has exactly one or two:

text
even node count on the diameter          odd node count on the diameter
A - B - C - D                            A - B - C - D - E
    ↑   ↑                                        ↑
  two centers → [B, C]                      one center → [C]

That is why the loop condition is while remaining > 2 and not while queue — the queue never empties on its own; you stop it.

Same machinery as Kahn’s, four differences (this is the whole template):

Template 1 (Kahn’s, DAG) Template 7 (leaf trimming, tree)
Edge insert one direction, in_degree[v] += 1 both directions, degree[u] += 1 and degree[v] += 1
Seed in_degree == 0 (sources) degree == 1 (leaves)
Stop queue empty → full ordering remaining <= 2 → the queue is the answer
Output the pop order the nodes never popped
python
def findMinHeightTrees(n, edges):
    """
    Find tree centroids by trimming leaves layer by layer.
    Time: O(V + E), Space: O(V)
    """
    from collections import deque

    # Edge case: single node
    if n == 1:
        return [0]

    # Build adjacency list and track degrees
    graph = [[] for _ in range(n)]
    degree = [0] * n

    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
        degree[u] += 1
        degree[v] += 1

    # Initialize queue with all leaf nodes (degree = 1)
    leaves = deque([i for i in range(n) if degree[i] == 1])

    # Trim leaves layer by layer
    remaining = n
    while remaining > 2:
        leaf_count = len(leaves)
        remaining -= leaf_count

        for _ in range(leaf_count):
            leaf = leaves.popleft()

            # Process neighbors of current leaf
            for neighbor in graph[leaf]:
                degree[neighbor] -= 1
                # If neighbor becomes a leaf, add to queue
                if degree[neighbor] == 1:
                    leaves.append(neighbor)

    # The remaining nodes (1 or 2) are the centroids
    return list(leaves)
java
// Java version
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
    // Edge case: single node
    if (n == 1) {
        return Collections.singletonList(0);
    }

    // Build adjacency list and track degrees
    List<List<Integer>> graph = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        graph.add(new ArrayList<>());
    }

    int[] degree = new int[n];

    for (int[] edge : edges) {
        int u = edge[0], v = edge[1];
        graph.get(u).add(v);
        graph.get(v).add(u);
        degree[u]++;
        degree[v]++;
    }

    // Initialize queue with all leaf nodes (degree = 1)
    Queue<Integer> leaves = new LinkedList<>();
    for (int i = 0; i < n; i++) {
        if (degree[i] == 1) {
            leaves.offer(i);
        }
    }

    // Trim leaves layer by layer
    int remaining = n;
    while (remaining > 2) {
        int leafCount = leaves.size();
        remaining -= leafCount;

        for (int i = 0; i < leafCount; i++) {
            int leaf = leaves.poll();

            // Process neighbors of current leaf
            for (int neighbor : graph.get(leaf)) {
                degree[neighbor]--;
                // If neighbor becomes a leaf, add to queue
                if (degree[neighbor] == 1) {
                    leaves.offer(neighbor);
                }
            }
        }
    }

    // The remaining nodes (1 or 2) are the centroids
    return new ArrayList<>(leaves);
}

Pitfalls specific to this template

  1. n == 1 makes the queue empty, not full: with no edges no node ever reaches degree 1, so the while never runs and you return []. Guard it. if n <= 2: return list(range(n)) covers both trivial sizes and reads better than a bare n == 1 check (V0-2 in the solution file).
  2. No visited set is needed. degree only ever decreases, so a node passes through the value 1 at most once and can be enqueued at most once — the tree structure does the deduplication.
  3. Count with a separate remaining, decremented by the layer size before draining the layer. Reading len(queue) after the fact is not the number of live nodes.
  4. degree[leaf] is not reset to 0 when the leaf is trimmed. Harmless: a trimmed leaf’s degree goes 1 → 0 when a sibling leaf later decrements it, and 0 never re-triggers the enqueue test.

Similar problems: see Tree Centroid Finding below.

Template 8: Union Find (Connected Components)

python
class UnionFind:
    """
    Union Find with path compression and union by rank.
    Time: O(α(N)) per operation (nearly O(1)), Space: O(N)
    """
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.count = n  # number of connected components

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # path compression
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return
        # union by rank
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        self.count -= 1

java
// Java version
class UnionFind {
    int[] parent;
    int[] rank;
    int count;

    UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        count = n;
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // path compression
        }
        return parent[x];
    }

    void union(int x, int y) {
        int px = find(x), py = find(y);
        if (px == py) return;
        // union by rank
        if (rank[px] < rank[py]) {
            parent[px] = py;
        } else if (rank[px] > rank[py]) {
            parent[py] = px;
        } else {
            parent[py] = px;
            rank[px]++;
        }
        count--;
    }
}

Template 9: Topological Sort + DP on an Implicit DAG Priority 5 of 5 — Must know — expect it in almost every loop

Key Idea: the graph is never given as an edge list — it is implied by a comparison rule (grid[a] < grid[b] ⇒ edge a → b). Build in/out-degrees on the fly, then peel with Kahn’s. The number of BFS layers is the longest path in the DAG (no cycles are possible because the edge rule is strictly increasing).

When to use: “longest increasing path / chain” on a grid or matrix, or any DP on a DAG where you need nodes processed in dependency order without recursion depth risk.

The twist vs Template 1: peel from the sinks (out-degree 0 = local maxima) instead of the sources, so each layer is one step of the DP. Peeling from sources works too — just reverse the comparison.

java
// java
// LC 329 - Longest Increasing Path in a Matrix
// IDEA: implicit DAG (cell -> strictly larger neighbour) + Kahn's peeling from sinks;
//       answer = number of peeling layers = longest path length
public class Solution {
    // time = O(m * n), space = O(m * n)
    private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int longestIncreasingPath(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0;
        int m = matrix.length, n = matrix[0].length;

        // step 1) out-degree = how many strictly-larger neighbours a cell points to
        int[][] outdeg = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int[] d : DIRS) {
                    int x = i + d[0], y = j + d[1];
                    if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
                        outdeg[i][j]++;
                    }
                }
            }
        }

        // step 2) seed the queue with the sinks (local maxima)
        Deque<int[]> q = new ArrayDeque<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (outdeg[i][j] == 0) q.offer(new int[]{i, j});
            }
        }

        // step 3) peel layer by layer; each layer = +1 on the longest path
        int length = 0;
        while (!q.isEmpty()) {
            length++;
            for (int sz = q.size(); sz > 0; sz--) {
                int[] cur = q.poll();
                int i = cur[0], j = cur[1];
                for (int[] d : DIRS) {
                    int x = i + d[0], y = j + d[1];
                    // walk backwards along the implicit edge (smaller neighbour)
                    if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] < matrix[i][j]) {
                        if (--outdeg[x][y] == 0) q.offer(new int[]{x, y});
                    }
                }
            }
        }
        return length;
    }
}
python
# python
# LC 329 - Longest Increasing Path in a Matrix
# IDEA: implicit DAG (cell -> strictly larger neighbour) + Kahn's peeling from sinks
from collections import deque

def longestIncreasingPath(matrix):
    # time = O(m * n), space = O(m * n)
    if not matrix or not matrix[0]:
        return 0

    m, n = len(matrix), len(matrix[0])
    dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))

    # step 1) out-degree = number of strictly-larger neighbours
    outdeg = [[0] * n for _ in range(m)]
    for i in range(m):
        for j in range(n):
            for di, dj in dirs:
                x, y = i + di, j + dj
                if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
                    outdeg[i][j] += 1

    # step 2) seed with sinks (local maxima)
    q = deque((i, j) for i in range(m) for j in range(n) if outdeg[i][j] == 0)

    # step 3) peel; layer count == longest increasing path
    length = 0
    while q:
        length += 1
        for _ in range(len(q)):
            i, j = q.popleft()
            for di, dj in dirs:
                x, y = i + di, j + dj
                if 0 <= x < m and 0 <= y < n and matrix[x][y] < matrix[i][j]:
                    outdeg[x][y] -= 1
                    if outdeg[x][y] == 0:
                        q.append((x, y))
    return length

Note: the classic alternative is DFS + memo (dp[i][j] = 1 + max(dp[neighbour])), also O(m·n). Prefer the Kahn’s version when the grid is large enough that recursion could blow the stack, or when the interviewer explicitly asks for topological sort.

Same shape, explicit graph: LC 1857 (Largest Color Value in a Directed Graph) — carry a count[node][26] DP array through Kahn’s instead of a single layer counter; if the queue drains before all nodes are popped, there is a cycle → return -1.


Second solution: memoised DFS

Top-down instead of peeling: “move only to a strictly larger value” makes the grid a DAG, so memo[cell] (the longest path starting there) is computed once, and no visited set is needed — the strict inequality already forbids walking back onto the current path. This is the version most interviewers expect first; the Kahn’s version above avoids recursion as deep as m·n.

python
# python
# LC 329 - Longest Increasing Path in a Matrix (memoised DFS)
# IDEA: implicit DAG (edges only go to strictly larger values) + memoised DFS
# time = O(m * n), space = O(m * n)
def longestIncreasingPath(matrix):
    m, n = len(matrix), len(matrix[0])
    memo = [[0] * n for _ in range(m)]      # 0 = not computed yet
    def dfs(i, j):
        if memo[i][j]:
            return memo[i][j]
        best = 1                            # the cell itself
        for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            x, y = i + di, j + dj
            if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
                best = max(best, 1 + dfs(x, y))
        memo[i][j] = best
        return best
    return max(dfs(i, j) for i in range(m) for j in range(n))
java
// java
// LC 329 - Longest Increasing Path in a Matrix (memoised DFS)
// IDEA: the "move only to a bigger value" rule makes the grid a DAG, so DFS + memo computes each cell once
// time = O(m * n), space = O(m * n)
class Solution {
    private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int longestIncreasingPath(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length, best = 0;
        int[][] memo = new int[m][n];   // 0 = not computed yet
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                best = Math.max(best, dfs(matrix, i, j, memo));
            }
        }
        return best;
    }

    private int dfs(int[][] mat, int i, int j, int[][] memo) {
        if (memo[i][j] != 0) {
            return memo[i][j];
        }
        int best = 1;
        for (int[] d : DIRS) {
            int x = i + d[0], y = j + d[1];
            if (x >= 0 && x < mat.length && y >= 0 && y < mat[0].length && mat[x][y] > mat[i][j]) {
                best = Math.max(best, 1 + dfs(mat, x, y, memo));
            }
        }
        memo[i][j] = best;
        return best;
    }
}

Template 10: In-Degree Signature (answer read straight off the degrees) Priority 4 of 5 — High value — a gap here costs you rounds

Key Idea: some “graph” problems never need the traversal at all — the answer is fully determined by in-degree / out-degree counts. Recognising this turns a Medium into 3 lines.

Key Insight (LC 1557): on a DAG, a node with in-degree 0 can only be reached by starting there, and every other node is reachable from some in-degree-0 node. So the set of in-degree-0 nodes is both necessary and sufficient — and therefore the unique minimum answer.

java
// java
// LC 1557 - Minimum Number of Vertices to Reach All Nodes
// IDEA: on a DAG, the minimal start set == exactly the nodes with in-degree 0
public class Solution {
    // time = O(V + E), space = O(V)
    public List<Integer> findSmallestSetOfVertices(int n, List<List<Integer>> edges) {
        boolean[] hasIncoming = new boolean[n];
        for (List<Integer> e : edges) {
            hasIncoming[e.get(1)] = true;
        }

        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (!hasIncoming[i]) res.add(i);
        }
        return res;
    }
}
python
# python
# LC 1557 - Minimum Number of Vertices to Reach All Nodes
# IDEA: on a DAG, the minimal start set == exactly the nodes with in-degree 0
def findSmallestSetOfVertices(n, edges):
    # time = O(V + E), space = O(V)
    has_incoming = [False] * n
    for _, v in edges:
        has_incoming[v] = True
    return [i for i in range(n) if not has_incoming[i]]

Variation A — degree signature lookup: LC 997 Find the Town Judge

LC Pattern — the tell is that the wanted node is described entirely by how many edges touch it, never by what it can reach:

Signal in the statement What it means
“the judge trusts nobody” out_degree == 0
“everybody except the judge trusts the judge” in_degree == n - 1
“there is exactly one such person” scan for the fingerprint; -1 if no node has it

Nothing here asks who reaches whom, so no adjacency list, no queue, no DFS — two counter arrays and two passes. Same family as LC 1557 (in_degree == 0) above; only the fingerprint changes.

LC 997 = find the node with in_degree == n - 1 and out_degree == 0.

Core Idea, step 1 — count both degrees literally. This is what to write first: it maps one-to-one onto the two sentences in the statement, so it cannot be mis-derived under pressure.

python
# python
# LC 997 - Find the Town Judge
# IDEA: judge == trusted by everyone else (in = n-1) and trusts nobody (out = 0)
def findJudge(n, trust):
    # time = O(V + E), space = O(V)
    # labels are 1..n, so size n + 1 and ignore index 0
    in_degree = [0] * (n + 1)
    out_degree = [0] * (n + 1)

    for a, b in trust:          # a trusts b
        out_degree[a] += 1
        in_degree[b] += 1

    for person in range(1, n + 1):
        if out_degree[person] == 0 and in_degree[person] == n - 1:
            return person
    return -1

Core Idea, step 2 — fold the two arrays into one. Worth the second variant because it halves the space and is the version most interviewers expect: track score = in_degree - out_degree and scan for n - 1.

Why the fold is safe (say this out loud — it is the only non-obvious step): in_degree <= n - 1 always, because the pairs in trust are unique and a != b, so at most n - 1 distinct people can trust you. Given that cap, in - out == n - 1 forces in == n - 1 and out == 0 — there is no way to reach n - 1 by having a large in-degree offset by a negative out-degree. If the problem ever allowed duplicate trust pairs, in_degree could exceed n - 1 and this fold would report a false judge; step 1 would still be correct.

java
// java
// LC 997 - Find the Town Judge
// IDEA: judge == in-degree (n-1) and out-degree 0  =>  score = in - out == n - 1
public class Solution {
    // time = O(V + E), space = O(V)
    public int findJudge(int n, int[][] trust) {
        int[] score = new int[n + 1];
        for (int[] t : trust) {
            score[t[0]]--;   // t[0] trusts someone -> out-degree
            score[t[1]]++;   // t[1] is trusted     -> in-degree
        }
        for (int i = 1; i <= n; i++) {
            if (score[i] == n - 1) return i;
        }
        return -1;
    }
}
python
# python
# LC 997 - Find the Town Judge
def findJudge(n, trust):
    # time = O(V + E), space = O(V)
    score = [0] * (n + 1)
    for a, b in trust:
        score[a] -= 1   # a trusts someone -> out-degree
        score[b] += 1   # b is trusted     -> in-degree
    for i in range(1, n + 1):
        if score[i] == n - 1:
            return i
    return -1

Pitfalls

  1. Labels are 1-indexed. Allocate n + 1 and loop 1..n; a size-n array is an off-by-one crash on person n.
  2. n == 1 with trust == [] must return 1. Both forms above get it for free — n - 1 == 0 and every score is 0 — which is a reason to prefer them over the set-based approach, which needs an explicit if n == 1: return 1 guard.
  3. Don’t stop at “trusted by someone and trusts nobody”. That is only a candidate. The count must be exactly n - 1; with n = 3, trust = [[1,3]] node 3 passes the weak test and is not the judge. The set-difference variant (trusted - trusting, V2 in the solution file) needs a separate n - 1 verification pass for exactly this reason — more code, same complexity.
  4. -1 is a real answer, not an error path: a cycle ([[1,3],[2,3],[3,1]]) leaves nobody with out_degree == 0.

Similar problems — the answer is a degree fingerprint

Problem Difficulty The fingerprint, and what changes
997 Find the Town Judge Easy in == n - 1 and out == 0 — the template
277 Find the Celebrity Medium same fingerprint, but edges are only reachable through a knows(a, b) API, so you cannot count degrees — eliminate candidates in one O(n) sweep, then verify the survivor’s row and column
2924 Find Champion II Medium in == 0 and it must be unique: exactly one in-degree-0 node ⇒ that node, otherwise -1
2923 Find Champion I Easy same as 2924, given as an adjacency matrix — find the row with no incoming 1
1557 Minimum Number of Vertices to Reach All Nodes Medium in == 0, but return all of them (Template 10 above)
1361 Validate Binary Tree Nodes Medium in <= 1 for all + exactly one in == 0 + a reachability pass (Variation B below)
4085 Find Root of a Colored Tree Medium edges are undirected; a per-edge color rule orients each one, then the root is the unique in == 0 (Variation C below)
2374 Node With Highest Edge Score Medium accumulate the sum of labels pointing in, not a count — same one-pass shape, different accumulator
1615 Maximal Network Rank Medium undirected, so one degree array; rank of a pair is deg(a) + deg(b) - 1 when they are adjacent

Variation B — degrees + one reachability pass: LC 1361 Validate Binary Tree Nodes

Twist: degrees alone are not enough. A valid binary tree needs three conditions — (1) every node has in-degree ≤ 1, (2) exactly one node has in-degree 0 (the root), and (3) all n nodes are reachable from that root. Conditions 1+2 without 3 still allow a valid-looking tree plus a detached cycle, which is exactly the trap case.

java
// java
// LC 1361 - Validate Binary Tree Nodes
// IDEA: in-degree <= 1 for all + exactly one in-degree-0 root + root reaches all n nodes
public class Solution {
    // time = O(N), space = O(N)
    public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
        // (1) no node may have two parents
        int[] indeg = new int[n];
        for (int i = 0; i < n; i++) {
            for (int c : new int[]{leftChild[i], rightChild[i]}) {
                if (c != -1 && ++indeg[c] > 1) return false;
            }
        }

        // (2) exactly one root
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (indeg[i] == 0) {
                if (root != -1) return false;   // 2+ roots => forest
                root = i;
            }
        }
        if (root == -1) return false;           // 0 roots => everything is in a cycle

        // (3) the root must reach every node (else a detached cycle exists)
        int seen = 1;
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            int node = stack.pop();
            for (int c : new int[]{leftChild[node], rightChild[node]}) {
                if (c != -1) {
                    seen++;
                    stack.push(c);
                }
            }
        }
        return seen == n;
    }
}
python
# python
# LC 1361 - Validate Binary Tree Nodes
def validateBinaryTreeNodes(n, leftChild, rightChild):
    # time = O(N), space = O(N)
    # (1) no node may have two parents
    indeg = [0] * n
    for i in range(n):
        for c in (leftChild[i], rightChild[i]):
            if c != -1:
                indeg[c] += 1
                if indeg[c] > 1:
                    return False

    # (2) exactly one root
    roots = [i for i in range(n) if indeg[i] == 0]
    if len(roots) != 1:
        return False

    # (3) root must reach every node
    seen, stack = 1, [roots[0]]
    while stack:
        node = stack.pop()
        for c in (leftChild[node], rightChild[node]):
            if c != -1:
                seen += 1
                stack.append(c)
    return seen == n

Variation C — orient every edge first, then read the degrees (global scan): LC 4085 Find Root of a Colored Tree

Twist: the edges arrive undirected, so there is no in-degree to count yet. The question asks for a root, and the obvious approach tries every node as the root and DFS-checks it, which is O(n²) and times out at n = 10⁵. The global scan instead asks each edge, once, which way it must point. If a local rule fixes every edge’s direction, the whole tree is oriented in one pass, and the root is just Template 10’s fingerprint: the only node with in == 0.

Core Idea — turn the global condition into a per-edge one:

  1. The three allowed color sequences are one cycle R → B → W → R, started at different colors. So “every depth has one color, following the cycle” means exactly color[child] == NEXT[color[parent]] on every edge.
  2. With three colors in a cycle, for an edge {u, v} with color[u] != color[v] exactly one of NEXT[u] == v / NEXT[v] == u holds, so the edge’s direction is forced. Equal colors satisfy neither, so no root can work → -1.
  3. After orienting all n - 1 edges, read the degrees: any in > 1 → -1 (two parents); any out > 2 → -1 (binary-tree property); otherwise the one node with in == 0 is the root.

Why no reachability pass, unlike LC 1361: the input is guaranteed to be a tree (connected, n - 1 edges). With every in ≤ 1 and n - 1 edges, exactly one node has in == 0. Connectivity is given, so a detached cycle (Variation B’s trap) cannot exist. The degree check is the whole proof.

text
global scan — pseudo code
    for each edge {u, v}:                      # one pass, O(1) per edge
        if   NEXT[color[u]] == color[v]:  parent, child = u, v
        elif NEXT[color[v]] == color[u]:  parent, child = v, u
        else: return -1                        # equal colors: no direction works
        in[child]  += 1
        out[parent] += 1
    for each node x:
        if in[x] > 1 or out[x] > 2: return -1  # two parents / three children
        if in[x] == 0: root = x                # exactly one, since edges = n - 1
    return root
text
trace — edges = [[3,1],[3,2],[1,4],[2,0]], colors = "BRRWB"
    node color : 0=B 1=R 2=R 3=W 4=B          NEXT: R->B, B->W, W->R
    edge 3-1 : W -> R is a step   => 3 -> 1
    edge 3-2 : W -> R is a step   => 3 -> 2
    edge 1-4 : R -> B is a step   => 1 -> 4
    edge 2-0 : R -> B is a step   => 2 -> 0
    in  = [1, 1, 1, 0, 1]   out = [0, 1, 1, 2, 0]
    no in > 1, no out > 2, in == 0 only at node 3   => return 3
python
# python
# LC 4085 - Find Root of a Colored Tree
# IDEA: each edge's color step forces its direction -> root = the only node with no parent
def findColoredRoot(edges, colors):
    # time = O(n), space = O(n)
    n = len(colors)
    # the color a child must have, given its parent's color
    next_color = {"R": "B", "B": "W", "W": "R"}

    parent_count = [0] * n
    child_count = [0] * n

    for u, v in edges:
        if next_color[colors[u]] == colors[v]:
            parent, child = u, v
        elif next_color[colors[v]] == colors[u]:
            parent, child = v, u
        else:
            # equal colors on an edge: whichever end is the parent, the color does not advance
            return -1
        parent_count[child] += 1
        child_count[parent] += 1

    root = -1
    for node in range(n):
        if parent_count[node] > 1:      # reached from two parents -> no orientation works
            return -1
        if child_count[node] > 2:       # binary-tree property
            return -1
        if parent_count[node] == 0:
            root = node
    return root
java
// java
// LC 4085 - Find Root of a Colored Tree
// IDEA: each edge's color step forces its direction -> root = the only node with no parent
public class Solution {
    // time = O(n), space = O(n)
    public int findColoredRoot(int[][] edges, String colors) {
        int n = colors.length();
        int[] parentCount = new int[n];
        int[] childCount = new int[n];

        for (int[] e : edges) {
            int u = e[0], v = e[1];
            int parent, child;
            if (nextColor(colors.charAt(u)) == colors.charAt(v)) {
                parent = u;
                child = v;
            } else if (nextColor(colors.charAt(v)) == colors.charAt(u)) {
                parent = v;
                child = u;
            } else {
                return -1;   // equal colors: no direction makes the color advance
            }
            parentCount[child]++;
            childCount[parent]++;
        }

        int root = -1;
        for (int node = 0; node < n; node++) {
            if (parentCount[node] > 1) return -1;   // two parents
            if (childCount[node] > 2) return -1;    // binary-tree property
            if (parentCount[node] == 0) root = node;
        }
        return root;
    }

    // the color a child must have, given its parent's color: R -> B -> W -> R
    private char nextColor(char c) {
        if (c == 'R') return 'B';
        if (c == 'B') return 'W';
        return 'R';
    }
}

Pitfalls

  1. Don’t try every root. That’s O(n) roots × O(n) DFS. The tell that a global scan works is that the condition on the whole tree breaks down into a condition on each edge (here, “the color advances by one step”).
  2. Equal colors on an edge is -1 immediately. Neither direction steps the cycle, so it is not “pick either”.
  3. Check out ≤ 2 on the root too. The root has no parent, so all of its up-to-3 edges are children; a degree-3 root fails the binary-tree property.
  4. Two parents is the only way the orientation can fail once every edge has a direction. With n - 1 edges and every in ≤ 1, the root is unique, so there’s no “two roots” case to handle.

Similar problems — fix a direction (or a candidate) per edge, then read the answer off the counts

Problem Difficulty What the scan settles, and what changes
4085 Find Root of a Colored Tree Medium the color step orients each edge; root = unique in == 0
1361 Validate Binary Tree Nodes Medium edges already directed; same in ≤ 1 + one root, but no connectivity guarantee, so a reachability pass is needed (Variation B)
2924 Find Champion II Medium directed DAG given; root/champion = unique in == 0
685 Redundant Connection II Hard directed edges; the node with in == 2 names the two candidate edges to delete, then union-find picks one
1791 Find Center of Star Graph Easy undirected; the center is the node shared by any two edges, a degree fingerprint with no orientation
310 Minimum Height Trees Medium the “try every root” problem with no local rule; leaf trimming (Template 7) replaces it
834 Sum of Distances in Tree Hard when an answer is needed for every root, use re-rooting DP (tree.md)

Problem Classification

Problem Difficulty Category Key Technique
207. Course Schedule Medium Course Scheduling Cycle Detection
210. Course Schedule II Medium Course Scheduling BFS/DFS
269. Alien Dictionary Hard Lexicographical Character Ordering
310. Minimum Height Trees Medium Tree Centroid Leaf Trimming
444. Sequence Reconstruction Medium Build Order Unique Ordering
630. Course Schedule III Hard Course Scheduling Greedy + Heap
802. Find Eventual Safe States Medium Cycle Detection Reverse Graph
851. Loud and Rich Medium Graph Layering DFS + Memoization
953. Verifying an Alien Dictionary Easy Lexicographical Order Validation
1059. All Paths from Source Lead to Destination Medium Cycle Detection DFS
1136. Parallel Courses Medium Task Scheduling Level BFS
1203. Sort Items by Groups Respecting Dependencies Hard Build Order Double Topological
1462. Course Schedule IV Medium Course Scheduling Transitive Closure
1857. Largest Color Value in a Directed Graph Hard Graph Layering DP on DAG
2050. Parallel Courses III Hard Task Scheduling Time Calculation
2115. Find All Possible Recipes from Given Supplies Medium Build Order Modified BFS
547. Number of Provinces Medium Connected Components Union Find / DFS
2192. All Ancestors of a Node in a Directed Acyclic Graph Medium Graph Layering DFS/BFS
329. Longest Increasing Path in a Matrix Hard Graph Layering Implicit DAG + Kahn’s (Template 9)
1557. Minimum Number of Vertices to Reach All Nodes Medium Graph Layering In-degree 0 set (Template 10)
997. Find the Town Judge Easy Degree Counting In/out-degree signature (Template 10-A)
1361. Validate Binary Tree Nodes Medium Cycle Detection In-degree + reachability (Template 10-B)
1245. Tree Diameter Medium Tree Centroid Leaf trimming layer count (Template 7)
2603. Collect Coins in a Tree Hard Tree Centroid Two-stage leaf trimming (Template 7)
277. Find the Celebrity Medium Degree Counting Same fingerprint as 997, API-only edges (Template 10-A)
2924. Find Champion II Medium Degree Counting Unique in-degree-0 node (Template 10-A)

Problem Patterns by Category

Course Scheduling Problems

Pattern Problems Key Insight
Basic Cycle Detection 207 Check if DAG exists
Find Valid Order 210 Return topological order
With Time Constraints 630 Greedy + priority queue
Query Prerequisites 1462 Floyd-Warshall/DFS

Task Scheduling Problems

Pattern Problems Key Insight
Minimum Time 1136, 2050 Level-wise BFS
Parallel Execution 1136 Count levels
With Durations 2050 DP on completion times

Lexicographical Ordering

Pattern Problems Key Insight
Character Order 269 Build graph from comparisons
Verify Order 953 Check consistency
Custom Comparator 269 Extract rules from examples

Build Order & Dependencies

Pattern Problems Key Insight
Unique Reconstruction 444 Queue size always 1
Recipe Dependencies 2115 Handle initial supplies
Group Dependencies 1203 Two-level topological sort

Graph Layering

Pattern Problems Key Insight
Find Ancestors 2192 Reverse graph traversal
Richest Reachable 851 DFS with memoization
Max Path Value 1857 DP on DAG
Longest Path on Implicit DAG 329 Edges implied by a < b; layer count = path length

Degree Counting (no traversal needed)

Pattern Problems Key Insight
Minimal Start Set 1557 On a DAG the answer is exactly the in-degree-0 nodes
Node Fingerprint 997, 277 Judge / celebrity = in-degree n-1 and out-degree 0; fold to score = in - out == n - 1
Unique Source 2924, 2923 Champion = the only in-degree-0 node, else -1
Weighted Degree 2374, 1615 Accumulate labels or plain degrees instead of counting edges
Validate Tree Shape 1361 in-degree ≤ 1 + one root + root reaches all n
Orient, then Count (global scan) 4085 A per-edge rule forces each undirected edge’s direction; root = the unique in-degree-0 node

Cycle Detection

Pattern Problems Key Insight
Safe States 802 Reverse graph + outdegree
All Paths Safe 1059 DFS with path tracking
Detect Any Cycle 207 Three-color DFS

Connected Components (Union Find / DFS)

Pattern Problems Key Insight
Count Components 547, 323 Union Find or DFS traversal
Detect Redundant Edge 684 Union Find cycle detection
Number of Islands 200 DFS/BFS on grid

Tree Centroid Finding

Same machinery — build a degree array, seed the queue with the leaves, peel inward (Template 7):

Problem Difficulty What changes vs LC 310
310 Minimum Height Trees Medium the template itself — peel to remaining <= 2, return the survivors
1245 Tree Diameter Medium read the layer count instead of the survivors: 2 * layers with one survivor, 2 * layers + 1 with two. (The textbook answer is two BFS passes; this reuses the same peel.)
2603 Collect Coins in a Tree Hard peel twice: first repeatedly drop leaves holding no coin, then drop exactly 2 more leaf layers; answer = 2 * remaining_edges
802 Find Eventual Safe States Medium the directed cousin — seed on out_degree == 0 and peel the reverse graph; the survivors are the unsafe nodes

Same tree, different tool — reach for these when the answer is not the middle of the tree:

Problem Difficulty Why leaf trimming does not apply
543 Diameter of Binary Tree Easy the tree is already rooted — one DFS returning height, no degrees
1522 Diameter of N-Ary Tree Medium same DFS, combine the top two child heights
834 Sum of Distances in Tree Hard needs every node’s answer → rerooting DP, not one peel
863 All Nodes Distance K in Binary Tree Medium BFS outward from a node (add parent links first), the opposite direction
1443 Minimum Time to Collect All Apples in a Tree Medium prunes by content (no apple in the subtree), not by degree

Decision Framework

text
START: Topological Sort Problem
│
├── Working with undirected tree?
│   │
│   ├── YES → Finding tree center/centroid?
│   │   │
│   │   ├── YES → Use Template 7 (Tree Centroid Finding)
│   │   │
│   │   └── NO → Continue
│   │
│   └── NO → Continue
│
├── Need all valid orderings?
│   │
│   ├── YES → Use Template 5 (Backtracking)
│   │
│   └── NO → Continue
│
├── Need lexicographical order?
│   │
│   ├── YES → Use Template 4 (Priority Queue)
│   │
│   └── NO → Continue
│
├── Need parallel execution time?
│   │
│   ├── YES → Use Template 6 (Level BFS)
│   │
│   └── NO → Continue
│
├── Need to count connected components?
│   │
│   ├── YES → Use Template 8 (Union Find)
│   │
│   └── NO → Continue
│
├── Need cycle detection only?
│   │
│   ├── YES → Use Template 2 (Three-Color DFS)
│   │
│   └── NO → Continue
│
└── DEFAULT → Use Template 1 (BFS Kahn's Algorithm)

The UnionFind class above is the template; LC 547 itself is worked by traversal in graph.md § Template 1, and by union-find in union_find_examples.md.

Worked Examples

The classic problems live in topology_sorting_examples.md, grouped by the template they exercise:

Group Problems The template they exercise
Kahn’s algorithm LC 210, 444, 269 Kahn’s (T1)
Parallel task scheduling LC 1136 parallel task scheduling (T6)
Leaf trimming LC 310 leaf trimming (T7)

LC 207 and LC 802 are the anchor problems of graph.md § Template 4 and § Template 5; LC 329 is worked on Template 9 above.

Summary & Interview Tips

Common Pitfalls

  1. Forgetting Cycle Detection: Always check if result size equals number of nodes
  2. Wrong Edge Direction: Remember edges go from prerequisite to dependent
  3. Not Handling Disconnected Components: Process all unvisited nodes
  4. Incorrect In-degree Initialization: Ensure all nodes are included
  5. Missing Edge Cases: Empty graph, single node, self-loops
  6. Confusing Degree vs In-degree: For undirected trees use total degree, for DAGs use in-degree
  7. Wrong Stopping Condition: For tree centroids, stop at ≤2 nodes (not when queue is empty)

Key Insights

  1. BFS vs DFS: BFS is easier for finding one order, DFS for all orders
  2. In-degree Tracking: Nodes with 0 in-degree can be processed
  3. Three-Color DFS: White (unvisited), Gray (visiting), Black (visited)
  4. Reverse Graph: Useful for problems like safe states
  5. Level Processing: For parallel execution and minimum time
  6. Tree Centroid Finding: For undirected trees, use degree (not in-degree), trim leaves layer by layer until 1-2 nodes remain
  7. Undirected vs Directed: Undirected trees need bidirectional edges and degree tracking, while DAGs use in-degree

Interview Approach

  1. Clarify Requirements:

    • Is the graph guaranteed to be a DAG?
    • Do we need all orderings or just one?
    • Are there any ordering preferences?
  2. Choose Algorithm:

    • Default to BFS (Kahn’s) for simplicity
    • Use DFS for recursive problems
    • Use priority queue for lexicographical order
  3. Handle Edge Cases:

    • Empty graph
    • Single node
    • Disconnected components
    • Cycles in graph
  4. Optimize if Needed:

    • Early termination for cycle detection
    • Space optimization with in-place modifications
    • Time optimization with better data structures

Time/Space Complexity Summary

Operation Time Space Notes
Build Graph O(E) O(V + E) Adjacency list
Calculate In-degrees O(E) O(V) Array or map
BFS/DFS Traversal O(V + E) O(V) Visit each node/edge once
Cycle Detection O(V + E) O(V) Three-color marking
All Orderings O(V! × E) O(V) Exponential for all permutations
  • Strongly Connected Components: Tarjan’s/Kosaraju’s algorithm
  • Shortest Path in DAG: Topological sort + relaxation
  • Critical Path Method: Project scheduling
  • Dependency Resolution: Package managers, build systems
  • Dataflow Analysis: Compiler optimization