Course Schedule
Interview Prep
The problem
You have num_courses courses labeled
0..num_courses-1. Some courses have prerequisites:
[a, b] means you must take course b
before course a. Decide whether you can finish all
courses. (Equivalently: does the directed prerequisite graph have
a cycle?)
Pattern: cycle detection in a DAG
A valid course ordering exists if and only if the prerequisite graph is a directed acyclic graph (DAG) — a cycle would mean some course is a (transitive) prerequisite of itself, which is impossible. Two textbook approaches: DFS with three-color marking detects back edges (= cycles), or Kahn's algorithm tries to peel off zero-indegree vertices and fails if any are left.
Both produce the topological order as a side effect if needed. The interviewer asks "can you finish" but the same code answers "what order should you take them in" with a one-line change.
Approach 1: DFS with three colors
Color each vertex WHITE (unvisited), GRAY (currently on the DFS stack), BLACK (fully processed). If a DFS edge ever points to a GRAY vertex, you've found a back edge — a cycle.
def can_finish_dfs(num_courses: int, prereq: list[list[int]]) -> bool:
graph = [[] for _ in range(num_courses)]
for a, b in prereq:
graph[b].append(a) # b must come before a
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * num_courses
def visit(u: int) -> bool:
color[u] = GRAY
for v in graph[u]:
if color[v] == GRAY:
return False # back edge — cycle
if color[v] == WHITE and not visit(v):
return False
color[u] = BLACK
return True
return all(color[u] == BLACK or visit(u) for u in range(num_courses)) Approach 2: Kahn's algorithm (BFS-style)
Maintain in-degree counts. Repeatedly "complete" any course with zero remaining prerequisites; each completion decrements the in-degree of its dependents. If every course is eventually completed, the graph is acyclic; if some are left with positive in-degree, they're inside a cycle.
from collections import deque
def can_finish_kahn(num_courses: int, prereq: list[list[int]]) -> bool:
graph = [[] for _ in range(num_courses)]
indeg = [0] * num_courses
for a, b in prereq:
graph[b].append(a)
indeg[a] += 1
q = deque(u for u in range(num_courses) if indeg[u] == 0)
completed = 0
while q:
u = q.popleft()
completed += 1
for v in graph[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return completed == num_courses Trace (acyclic case)
num_courses = 4
prereq = [[1, 0], [2, 1], [3, 2]] # 0 → 1 → 2 → 3 (linear chain)
graph = [[1], [2], [3], []]
indeg = [0, 1, 1, 1]
Initial queue: [0]
Step 1: pop 0, completed=1, decrement indeg of [1] → indeg=[0,0,1,1], queue=[1]
Step 2: pop 1, completed=2, decrement indeg of [2] → indeg=[0,0,0,1], queue=[2]
Step 3: pop 2, completed=3, decrement indeg of [3] → indeg=[0,0,0,0], queue=[3]
Step 4: pop 3, completed=4, no neighbors, queue=[]
return completed == 4 → True Trace (cyclic case)
num_courses = 2
prereq = [[1, 0], [0, 1]] # 0 → 1 → 0 (cycle)
graph = [[1], [0]]
indeg = [1, 1]
Initial queue: [] (no zero-indegree vertices)
Loop never executes. completed = 0.
return 0 == 2 → False Complexity
- Time:
O(V + E)for both approaches — every vertex and edge processed once. - Space:
O(V + E)for the adjacency list, plusO(V)for the color/indegree array and queue.
Variations worth knowing
- Course Schedule II: return a valid order, not
just feasibility. Append
uto the answer list as it's completed in Kahn's (or as it turns BLACK in DFS — but DFS gives reverse order, so reverse at the end). - Course Schedule IV (transitive prerequisites):
multiple queries of "is X a prerequisite of Y?" Precompute the
transitive closure with Floyd-Warshall
O(V^3)or viaVDFS callsO(V(V+E)). - Critical path / longest chain: the longest path
in a DAG. DP on the topological order:
longest[u] = 1 + max(longest[v] for v in graph[u]). - Parallel scheduling: minimum number of semesters to finish all courses, taking unlimited courses in parallel each semester. The answer is the length of the longest path in the DAG.