LeetCode ยท #207

Course Schedule Solution

Given numCourses courses and a list of prerequisite pairs, determine whether it is possible to finish all courses โ€” i.e., whether the prerequisite graph contains a cycle.

01
Section One ยท Problem

Problem Statement

๐Ÿท๏ธ

Difficulty

Medium

๐Ÿ”—

LeetCode

Problem #207

๐Ÿ—๏ธ

Pattern

Graph โ€” topological sort + cycle detection

There are numCourses courses labeled 0 to numCourses-1. Given a list prerequisites where prerequisites[i] = [a, b] means you must take course b before course a, return true if all courses can be finished.

Example
Input: numCourses = 2, prerequisites = [[1,0]] Output: true // Take course 0, then course 1. No cycle. Input: numCourses = 2, prerequisites = [[1,0],[0,1]] Output: false // 0 requires 1, and 1 requires 0 โ€” circular dependency โ†’ impossible
Constraints
โ€ข 1 โ‰ค numCourses โ‰ค 2000 โ€ข 0 โ‰ค prerequisites.length โ‰ค 5000 โ† sparse graph, O(V+E) expected โ€ข prerequisites[i].length == 2 โ€ข No duplicate edges
02
Section Two ยท Approach 1

DFS Cycle Detection โ€” O(V+E)

Build an adjacency list from prerequisites. Run DFS from every unvisited node. Track three states per node: unvisited (0), in current DFS path (1), fully processed (2). If DFS reaches a node in state 1, a cycle exists. If any cycle is found, return false.

Why it works

A directed graph has a cycle if and only if a DFS encounters a "back edge" โ€” an edge pointing to an ancestor in the current DFS tree. The three-color marking (white/grey/black) reliably detects back edges without re-exploring completed subgraphs.

Why we can also use Kahn's algorithm
Alternative insight: Kahn's BFS-based topological sort is often clearer for explaining in interviews. Start with all nodes whose in-degree is 0 (no prerequisites). Process them, reducing in-degrees of their neighbors. If all V nodes are eventually processed, no cycle exists. If the queue empties before processing all nodes, a cycle trapped those nodes with in-degree > 0 forever.
Java โ€” DFS 3-color cycle detection
import java.util.*; class Solution { public boolean canFinish(int n, int[][] pre) { List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (int[] p : pre) adj.get(p[1]).add(p[0]); int[] state = new int[n]; // 0=unvisited, 1=visiting, 2=done for (int i = 0; i < n; i++) if (hasCycle(adj, state, i)) return false; return true; } private boolean hasCycle(List<List<Integer>> adj, int[] st, int v) { if (st[v] == 1) return true; // back edge โ€” cycle found if (st[v] == 2) return false; // already fully explored st[v] = 1; for (int nb : adj.get(v)) if (hasCycle(adj, st, nb)) return true; st[v] = 2; return false; } }
Python โ€” DFS 3-color cycle detection
class Solution: def canFinish(self, n: int, prerequisites: list[list[int]]) -> bool: adj = [[] for _ in range(n)] for a, b in prerequisites: adj[b].append(a) state = [0] * n # 0=unvisited 1=visiting 2=done def dfs(v: int) -> bool: if state[v] == 1: return True # back edge if state[v] == 2: return False state[v] = 1 if any(dfs(nb) for nb in adj[v]): return True state[v] = 2 return False return not any(dfs(i) for i in range(n) if state[i] == 0)
MetricValue
TimeO(V + E) โ€” each node and edge visited once
SpaceO(V + E) โ€” adjacency list + O(V) state + O(V) call stack
03
Section Three ยท Approach 2

Kahn's BFS Topological Sort โ€” O(V+E)

Count the in-degree of every node. Enqueue all nodes with in-degree 0. Process: dequeue a node, decrement neighbors' in-degrees, enqueue any that reach 0. If all V nodes are processed, return true; otherwise a cycle prevented some nodes from ever reaching in-degree 0.

๐Ÿ’ก Mental model: Imagine a university registration system. A course can only open enrollment once all its prerequisites have been completed. Start by opening every course with no prerequisites. When a course is completed, lower the "remaining prerequisites" counter for every course that depended on it. Any course whose counter hits zero opens next. If all courses eventually open, the curriculum is acyclic. If some courses never open (their counter never reaches zero), there's a circular dependency that blocks them forever.
Algorithm โ€” Kahn's BFS
  • Build adjacency list from prerequisites: b โ†’ a for each [a, b] pair.
  • Compute inDegree[i] for each node i.
  • Enqueue all nodes where inDegree[i] == 0.
  • BFS: dequeue node, increment processed, for each neighbor decrement inDegree, enqueue if it reaches 0.
  • Return processed == numCourses.
๐ŸŽฏ When to recognize this pattern:
  • Any "dependency ordering" or "can all tasks be done?" problem.
  • Signal words: "prerequisite," "dependency," "order," "schedule." Kahn's BFS gives topological order as a side effect (LC 210), while the DFS approach is slightly more compact for pure cycle detection.
  • Both are O(V+E) โ€” choose whichever you can explain clearly.
Why processed == V proves no cycle:
  • Every node with in-degree 0 is eventually enqueued.
  • If a cycle exists, all nodes in the cycle have in-degree โ‰ฅ 1 always (they depend on each other) and are never enqueued.
  • So fewer than V nodes are processed โ€” the deficit is exactly the cycle size.
04
Section Four ยท Trace

Visual Walkthrough

Trace Kahn's BFS on numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]. Graph: 0โ†’1, 0โ†’2, 1โ†’3, 2โ†’3.

Kahn's BFS โ€” Course Schedule (4 courses, no cycle)
In-degree tracking. Queue shows nodes ready to process. Green = processed. Prerequisite graph: 0โ†’1, 0โ†’2, 1โ†’3, 2โ†’3 0 1 2 3 In-degree table (initial): Node 0: in-degree = 0 โ† enqueue immediately Node 1: in-degree = 1 (from 0) Node 2: in-degree = 1 (from 0) Node 3: in-degree = 2 (from 1 and 2) Step 1: Dequeue 0. processed=1. Decrement in-degree of 1 (โ†’0) and 2 (โ†’0). Enqueue 1 and 2. Queue: [1, 2] Step 2: Dequeue 1. processed=2. Decrement in-degree of 3 (2โ†’1). Not yet 0 โ€” don't enqueue. Queue: [2] Step 3: Dequeue 2. processed=3. Decrement in-degree of 3 (1โ†’0). Enqueue 3. Queue: [3] Step 4: Dequeue 3. processed=4. No outgoing edges. Queue empty. Queue: [] processed (4) == numCourses (4) โ†’ true โœ“
05
Section Five ยท Implementation

Code โ€” Java & Python

Java โ€” Kahn's BFS topological sort
import java.util.*; class Solution { public boolean canFinish(int n, int[][] prerequisites) { List<List<Integer>> adj = new ArrayList<>(); int[] inDeg = new int[n]; for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); for (int[] p : prerequisites) { adj.get(p[1]).add(p[0]); // p[1] โ†’ p[0] inDeg[p[0]]++; } Queue<Integer> q = new LinkedList<>(); for (int i = 0; i < n; i++) if (inDeg[i] == 0) q.offer(i); // no prerequisites โ€” start here int processed = 0; while (!q.isEmpty()) { int v = q.poll(); processed++; for (int nb : adj.get(v)) { if (--inDeg[nb] == 0) q.offer(nb); // all prereqs met } } return processed == n; // false if cycle trapped nodes } }
Python โ€” Kahn's BFS topological sort
from collections import deque class Solution: def canFinish(self, n: int, prerequisites: list[list[int]]) -> bool: adj = [[] for _ in range(n)] in_deg = [0] * n for a, b in prerequisites: adj[b].append(a) in_deg[a] += 1 q = deque(i for i in range(n) if in_deg[i] == 0) processed = 0 while q: v = q.popleft() processed += 1 for nb in adj[v]: in_deg[nb] -= 1 if in_deg[nb] == 0: q.append(nb) # all prerequisites satisfied return processed == n
06
Section Six ยท Analysis

Complexity Analysis

ApproachTimeSpaceTrade-off
Brute force (try all orderings)O(V!)O(V)Completely infeasible for V > 10
DFS 3-color cycle detectionO(V + E)O(V + E)Recursive โ€” stack overflow risk for large V; harder to extend to ordering
Kahn's BFS โ† optimal O(V + E) O(V + E) Iterative โ€” no stack overflow; yields topological order as side effect
Why not just check the adjacency matrix for cycles?:
  • An adjacency matrix is O(Vยฒ) space โ€” for V=2000 that's 4M entries.
  • More critically, reading the matrix to detect cycles still requires graph traversal.
  • There's no shortcut to O(V+E) cycle detection other than actually traversing the graph.
07
Section Seven ยท Edge Cases

Edge Cases & Pitfalls

CaseInputWhy It Matters
No prerequisitesn=5, prerequisites=[]All in-degrees 0 โ†’ all enqueued immediately โ†’ processed=5=n โ†’ true.
Direct cycle (2 nodes)[[1,0],[0,1]]Both have in-degree 1. Neither enters queue. processed=0 < 2 โ†’ false.
Indirect cycle (3+ nodes)[[1,0],[2,1],[0,2]]All in-degrees = 1. Queue stays empty. processed=0 โ†’ false.
Single noden=1, prerequisites=[]In-degree 0, enqueued, processed=1=n โ†’ true.
Self-loop[[0,0]]Node 0 depends on itself. in-degree = 1, never reaches 0 โ†’ false.
Disconnected graphSome nodes with no edgesIsolated nodes have in-degree 0 and are enqueued and processed. Cycle only in one component still caught.
โš  Common Mistake: Reversing the edge direction. The prerequisite [a, b] means "must take b before a" โ€” the edge in the dependency graph is b โ†’ a. If you add a โ†’ b instead, you reverse all edges and compute in-degrees incorrectly. Node b would get in-degree incremented when it should be node a. Always double-check: the node that must be taken first (b) is the source; the node that depends on it (a) is the destination.

โ† Back to Graph problems