Two approaches to LeetCode Course Schedule โ DFS cycle detection O(V+E) and Kahn's BFS topological sort O(V+E). Java and Python solutions with visual walkthrough.
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.
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
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.*;
classSolution {
publicbooleancanFinish(int n, int[][] pre) {
List<List<Integer>> adj = newArrayList<>();
for (int i = 0; i < n; i++) adj.add(newArrayList<>());
for (int[] p : pre) adj.get(p[1]).add(p[0]);
int[] state = newint[n]; // 0=unvisited, 1=visiting, 2=donefor (int i = 0; i < n; i++)
if (hasCycle(adj, state, i)) returnfalse;
returntrue;
}
privatebooleanhasCycle(List<List<Integer>> adj, int[] st, int v) {
if (st[v] == 1) returntrue; // back edge โ cycle foundif (st[v] == 2) returnfalse; // already fully explored
st[v] = 1;
for (int nb : adj.get(v))
if (hasCycle(adj, st, nb)) returntrue;
st[v] = 2;
returnfalse;
}
}
Python โ DFS 3-color cycle detection
classSolution:
defcanFinish(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=donedefdfs(v: int) -> bool:
if state[v] == 1: returnTrue# back edgeif state[v] == 2: returnFalse
state[v] = 1if any(dfs(nb) for nb in adj[v]): returnTrue
state[v] = 2returnFalsereturn not any(dfs(i) for i in range(n) if state[i] == 0)
Metric
Value
Time
O(V + E) โ each node and edge visited once
Space
O(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.
Kahn's BFS โ Course Schedule (4 courses, no cycle)
05
Section Five ยท Implementation
Code โ Java & Python
Java โ Kahn's BFS topological sort
import java.util.*;
classSolution {
publicbooleancanFinish(int n, int[][] prerequisites) {
List<List<Integer>> adj = newArrayList<>();
int[] inDeg = newint[n];
for (int i = 0; i < n; i++) adj.add(newArrayList<>());
for (int[] p : prerequisites) {
adj.get(p[1]).add(p[0]); // p[1] โ p[0]
inDeg[p[0]]++;
}
Queue<Integer> q = newLinkedList<>();
for (int i = 0; i < n; i++)
if (inDeg[i] == 0) q.offer(i); // no prerequisites โ start hereint 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
classSolution:
defcanFinish(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 = 0while q:
v = q.popleft()
processed += 1for nb in adj[v]:
in_deg[nb] -= 1if in_deg[nb] == 0:
q.append(nb) # all prerequisites satisfiedreturn processed == n
06
Section Six ยท Analysis
Complexity Analysis
Approach
Time
Space
Trade-off
Brute force (try all orderings)
O(V!)
O(V)
Completely infeasible for V > 10
DFS 3-color cycle detection
O(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
Case
Input
Why It Matters
No prerequisites
n=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 node
n=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 graph
Some nodes with no edges
Isolated 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.