Course Schedule
Difficulty: Medium Source: NeetCode
Problem
There are a total of
numCoursescourses you have to take, labeled from0tonumCourses - 1. You are given an arrayprerequisiteswhereprerequisites[i] = [ai, bi]indicates that you must take coursebifirst if you want to take courseai.Return
trueif you can finish all courses. Otherwise, returnfalse.Example 1: Input:
numCourses = 2, prerequisites = [[1,0]]Output:trueExample 2: Input:
numCourses = 2, prerequisites = [[1,0],[0,1]]Output:falseConstraints:
1 <= numCourses <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 2- All prerequisite pairs are unique
- No self-loops
Prerequisites
Before attempting this problem, you should be comfortable with:
- Directed graphs — modeling courses as nodes and prerequisites as directed edges
- Cycle detection in directed graphs — the core problem reduces to this
- Topological sort — a valid ordering only exists if no cycle is present
1. DFS with Three-Color Cycle Detection
Intuition
Build a directed graph from prerequisites (edge from b to a means “take b before a”). The courses can be completed if and only if this graph has no cycle. DFS can detect cycles using three states: WHITE (unvisited), GRAY (currently in the DFS stack — part of the current path), and BLACK (fully processed). If DFS reaches a GRAY node, we’ve found a cycle.
Algorithm
- Build adjacency list from prerequisites.
- Mark all nodes as
WHITE(0). - For each unvisited node, run DFS:
- Mark it
GRAY(1) when entering. - For each neighbor: if
GRAY, cycle found — returnFalse; ifWHITE, recurse. - Mark it
BLACK(2) when done.
- Mark it
- Return
Trueif no cycle found.
Solution
def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = [[] for _ in range(numCourses)]
for a, b in prerequisites:
graph[b].append(a) # b must come before a
# 0 = unvisited, 1 = in-stack (GRAY), 2 = done (BLACK)
state = [0] * numCourses
def dfs(node):
if state[node] == 1: # cycle detected
return False
if state[node] == 2: # already processed, safe
return True
state[node] = 1 # mark as in-stack
for neighbor in graph[node]:
if not dfs(neighbor):
return False
state[node] = 2 # mark as done
return True
for course in range(numCourses):
if not dfs(course):
return False
return True
print(canFinish(2, [[1,0]])) # True
print(canFinish(2, [[1,0],[0,1]])) # False
print(canFinish(5, [[1,0],[2,1],[3,2],[4,3]])) # True (chain, no cycle)
print(canFinish(3, [[1,0],[2,1],[0,2]])) # False (cycle)
Complexity
- Time:
O(V + E)where V = numCourses, E = len(prerequisites) - Space:
O(V + E)for graph and recursion stack
2. Topological Sort (Kahn’s BFS Algorithm)
Intuition
A directed graph is acyclic (a DAG) if and only if it has a valid topological ordering. Kahn’s algorithm builds this ordering using in-degrees: repeatedly pick nodes with in-degree 0 (no remaining prerequisites), remove them, and decrement their neighbors’ in-degrees. If we can process all numCourses nodes this way, no cycle exists. If the queue empties before processing all nodes, there’s a cycle.
Algorithm
- Compute in-degree for each node.
- Enqueue all nodes with in-degree 0.
- While queue is non-empty: dequeue a node, increment
processedcount, decrement neighbors’ in-degrees. Enqueue neighbors that reach in-degree 0. - Return
processed == numCourses.
Solution
from collections import deque
def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = [[] for _ in range(numCourses)]
in_degree = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
in_degree[a] += 1
queue = deque(course for course in range(numCourses) if in_degree[course] == 0)
processed = 0
while queue:
course = queue.popleft()
processed += 1
for neighbor in graph[course]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return processed == numCourses
print(canFinish(2, [[1,0]])) # True
print(canFinish(2, [[1,0],[0,1]])) # False
print(canFinish(1, [])) # True
print(canFinish(3, [[1,0],[2,1],[0,2]])) # False
Complexity
- Time:
O(V + E) - Space:
O(V + E)
Common Pitfalls
Edge direction. prerequisites[i] = [a, b] means b → a (take b first). When building the adjacency list, make sure you add an edge from b to a, not the other way around.
Rerunning DFS on already-processed nodes. If state[node] == 2 (BLACK), the node was already confirmed cycle-free — return True immediately without processing it again. Skipping this check causes redundant work and potential stack overflows.
Kahn’s: missing isolated nodes. Nodes with no prerequisites start with in-degree 0 and must be seeded into the initial queue. If you forget them, processed won’t reach numCourses even when no cycle exists.