Model courses as nodes and each prerequisite pair [a, b] as a directed edge a -> b (βto finish a, first finish bβ). Finishing all courses is possible if and only if this directed graph has no cycle β a cycle means a set of courses that all depend on each other, so none of them can ever be first.
Running example: numCourses = 4, prerequisites = [[1,0],[2,1],[3,2],[1,3]] (course 1 needs 0; 2 needs 1; 3 needs 2; but 1 also needs 3 β a cycle among 1, 2, 3).
| Course | Depends on (edges out) |
|---|---|
| 0 | (none) |
| 1 | 0, 3 |
| 2 | 1 |
| 3 | 2 |
DFS With Three-Color Cycle Detection
Time O(V + E)Space O(V + E)Do a DFS from every course, coloring each node unvisited -> visiting -> done. If DFS ever reaches a node that is currently visiting (an ancestor in the current recursion path), that is a back edge, meaning a cycle.
class Solution: def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool: graph = [[] for _ in range(numCourses)] for a, b in prerequisites: graph[a].append(b)
UNVISITED, VISITING, DONE = 0, 1, 2 state = [UNVISITED] * numCourses
def dfs(u): if state[u] == VISITING: return False # back edge -> cycle if state[u] == DONE: return True # already confirmed safe
state[u] = VISITING for v in graph[u]: if not dfs(v): return False state[u] = DONE return True
return all(dfs(u) for u in range(numCourses))Tracing on the example, starting dfs(0) through dfs(3) in order:
dfs(0): mark 0VISITING. No dependencies. Mark 0DONE. ReturnsTrue.dfs(1): mark 1VISITING. Recurse into dependency 0 (DONE, returnsTrueimmediately). Recurse into dependency 3.dfs(3): mark 3VISITING. Recurse into dependency 2.dfs(2): mark 2VISITING. Recurse into dependency 1 β but 1 is currentlyVISITING(itβs the caller three frames up!). This is a back edge:dfs(1)returnsFalseimmediately, which propagates back throughdfs(2),dfs(3), anddfs(1), and the overall function returnsFalse.
Why itβs correct: a node colored VISITING is exactly a node currently on the DFS call stack β an ancestor of the current node in the DFS tree. Reaching it again means there is a path from it back to itself, i.e. a cycle. Complexity: each node is fully processed (colored DONE) once and each edge is traversed once β O(V + E) time; the color array, adjacency list, and recursion stack are all O(V + E) space.
Kahn's Algorithm (BFS Topological Sort)
OptimalTime O(V + E)Space O(V + E)Repeatedly peel off courses that have no remaining unfinished prerequisites (in-degree 0 in the βdepends onβ sense) β track it as out-degree in a graph pointing prerequisite β dependent instead. If every course eventually gets peeled off, there is no cycle. This avoids recursion entirely, which matters for very deep dependency chains.
from collections import deque
class Solution: def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool: graph = [[] for _ in range(numCourses)] indegree = [0] * numCourses for a, b in prerequisites: graph[b].append(a) # b unlocks a indegree[a] += 1
queue = deque(u for u in range(numCourses) if indegree[u] == 0) finished = 0
while queue: u = queue.popleft() finished += 1 for v in graph[u]: indegree[v] -= 1 if indegree[v] == 0: queue.append(v)
return finished == numCoursesWatch it on the first statement example β n = 2, prerequisites = [[1,0]]. Course 0 is ready from the start; finishing it redeems the one prerequisite holding course 1 back, and the readiness ripples through the queue:
Courses(badge = prerequisites still needed)
Prerequisite edges (left unlocks right)
Queue β front on the left, next to pop
Result order β 0 / 2 courses scheduled
First, count the in-degree of every course: how many prerequisites it still needs. Both courses start at 0, and each prerequisite pair adds 1 to the course that depends on it.
Now the running example from the top of the page β n = 4, prerequisites = [[1,0],[2,1],[3,2],[1,3]], where course 1 must wait for both 0 and 3. Watch the queue run dry β and what that emptiness proves about the courses left behind:
Courses(badge = prerequisites still needed)
Prerequisite edges (left unlocks right)
Queue β front on the left, next to pop
Result order β 0 / 4 courses scheduled
Same algorithm, now on the running example: 4 courses, pairs [1,0], [2,1], [3,2], and [1,3]. Pair [1,0]: course 1 needs course 0, so indegree[1] becomes 1.
Why itβs correct: a course can only be queued once every prerequisite has already been processed. If a cycle exists, every course in it perpetually has at least one un-processed prerequisite (another course in the same cycle), so it can never reach in-degree 0 and never gets counted β exactly what finished == numCourses checks. Complexity: each node is enqueued once and each edge decrements exactly one in-degree counter β O(V + E) time, O(V + E) space for the graph and in-degree array. Same asymptotic cost as the DFS version, but iterative, avoiding any recursion-depth concerns on graphs with long dependency chains.