Skip to content

207. Course Schedule (Medium)

Problem

You are given numCourses courses labeled 0 to numCourses - 1 and an array prerequisites where prerequisites[i] = [a, b] means you must take course b before course a. Return true if you can finish all courses, i.e., the prerequisite graph has no cycles.

Example

  • numCourses = 2, prerequisites = [[1, 0]]true
  • numCourses = 2, prerequisites = [[1, 0], [0, 1]]false

LeetCode 207 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, DFS with path tracking

For each course, DFS tracking the current recursion path; if we hit a course already on the path, it’s a cycle.

from collections import defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
for a, b in prerequisites: # L1: O(E) build adjacency list
graph[b].append(a)
def has_cycle(start):
on_path = set() # L2: O(1) init per call
def dfs(n):
if n in on_path: # L3: O(1) membership check
return True
on_path.add(n) # L4: O(1)
for nb in graph[n]: # L5: iterate neighbors
if dfs(nb): # L6: recurse
return True
on_path.remove(n) # L7: O(1) backtrack
return False
return dfs(start)
for c in range(num_courses): # L8: call has_cycle for every node
if has_cycle(c):
return False
return True

Where the time goes, line by line

Variables: V = numCourses, E = len(prerequisites).

LinePer-call costTimes executedContribution
L1 (build graph)O(1)O(1) per edgeEO(E)O(E)
L8 (outer loop)variesVV invocations of has_cycle
L3, L4, L7 (path ops)O(1)O(1)per node visited-
L5, L6 (DFS traversal)O(V+E)O(V + E) per startVO(V)O(V) calls × O(V+E)O(V + E) each
L8 totalO(V+E)O(V + E)VO(V2+VE)O(V² + VE) ← dominates

Because on_path is reset on every call to has_cycle, there is no memoization. A node reachable from k different starting points is fully re-explored k times. In a dense graph (E near V²), this blows up to O(V3)O(V³).

Complexity

  • Time: O(V2+VE)O(V² + VE), driven by L8 (re-walking the graph from every node).
  • Space: O(V+E)O(V + E).

Correct but wasteful.

Approach 2: DFS with three-color cycle detection (optimal)

Each node is white (unvisited), gray (on the current DFS path), or black (finished). A gray-to-gray transition means a cycle.

from collections import defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
for a, b in prerequisites: # L1: O(E) build graph
graph[b].append(a)
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * num_courses # L2: O(V) init color array
def dfs(n):
if color[n] == GRAY: # L3: O(1) back-edge check
return False # cycle
if color[n] == BLACK: # L4: O(1) already done
return True
color[n] = GRAY # L5: O(1) mark in-progress
for nb in graph[n]: # L6: iterate neighbors
if not dfs(nb): # L7: recurse
return False
color[n] = BLACK # L8: O(1) mark complete
return True
for c in range(num_courses): # L9: outer loop over all nodes
if not dfs(c):
return False
return True

Where the time goes, line by line

Variables: V = numCourses, E = len(prerequisites).

LinePer-call costTimes executedContribution
L1 (build graph)O(1)O(1) per edgeEO(E)O(E)
L2 (init colors)O(V)O(V)1O(V)O(V)
L3, L4 (color checks)O(1)O(1)once per node entryO(V)O(V) total
L5, L8 (color transitions)O(1)O(1)each node goes WHITE→GRAY→BLACK onceO(V)O(V) total
L6, L7 (edge traversal)O(1)O(1) per edgeeach edge visited at most onceO(E)O(E) ← dominates

The BLACK check at L4 is the memoization that makes this O(V+E)O(V + E) instead of O(V2+VE)O(V² + VE). Once a node is colored BLACK, any future DFS that reaches it returns immediately without re-exploring its subtree.

Complexity

  • Time: O(V+E)O(V + E), driven by L6/L7 (each edge visited at most once across the whole run).
  • Space: O(V+E)O(V + E).

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
DFS from every nodeO(V2+VE)O(V² + VE)O(V+E)O(V + E)
DFS + three-colorO(V+E)O(V + E)O(V+E)O(V + E)
Kahn’s algorithm (BFS)O(V+E)O(V + E)O(V+E)O(V + E)

Both DFS+color and Kahn’s are optimal. Kahn’s generalizes directly to problem 210 where we need the actual ordering, not just feasibility.

Test cases

# Quick smoke tests, paste into a REPL or save as test_207.py and run.
# Uses the canonical implementation (Approach 2: DFS three-color).
from collections import defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
for a, b in prerequisites:
graph[b].append(a)
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * num_courses
def dfs(n):
if color[n] == GRAY:
return False
if color[n] == BLACK:
return True
color[n] = GRAY
for nb in graph[n]:
if not dfs(nb):
return False
color[n] = BLACK
return True
for c in range(num_courses):
if not dfs(c):
return False
return True
def _run_tests():
# Example 1: simple chain, no cycle
assert can_finish(2, [[1, 0]]) == True
# Example 2: direct cycle
assert can_finish(2, [[1, 0], [0, 1]]) == False
# Edge: no prerequisites, trivially true
assert can_finish(5, []) == True
# Edge: single course, no prerequisites
assert can_finish(1, []) == True
# Larger cycle (3 nodes)
assert can_finish(3, [[1, 0], [2, 1], [0, 2]]) == False
# Larger DAG (no cycle)
assert can_finish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]) == True
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Graphs, cycle detection; topological sort
  • Hash Tables, adjacency list (defaultdict)
  • Cycle Detection, repeated-state tactics for finding loops in linked lists, graphs, arrays, and numeric processes.
  • Topological Sort, dependency-order tactics for DAGs, prerequisites, and detecting cycles in directed graphs.