TL;DR
Topological sort of the prerequisite graph β Kahnβs BFS emits the order directly (or DFS reverse post-order), both O(V + E) time and space; return [] on a cycle.
Approach 1 β Brute force (repeatedly append any takeable course)
Naively: keep scanning for a not-yet-taken course whose prerequisites are all taken, append it, repeat until a full pass makes no progress. If you took everyone, return the order; otherwise return [].
class Solution:
def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]:
taken = [False] * numCourses
order = []
while True:
progressed = False
for course in range(numCourses):
if taken[course]:
continue
if all(taken[b] for a, b in prerequisites if a == course):
taken[course] = True
order.append(course)
progressed = True
if not progressed:
break
return order if len(order) == numCourses else []
Complexity: up to numCourses passes, each rescanning all prerequisites β O(V Β· E). This is Kahnβs algorithm with the in-degree bookkeeping thrown away; restore it to get linear time.
Approach 2 β Kahnβs algorithm (BFS topological sort)
The insight: a course is safe to place next exactly when its in-degree is 0 (all prerequisites already placed). Maintain a queue of such courses; each time you place one, decrement its dependents and enqueue any that reach 0. The append order is itself a valid topological order. If fewer than numCourses courses come out, a cycle trapped the rest β return [].
from collections import deque
class Solution:
def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]:
adj = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for a, b in prerequisites: # edge b -> a
adj[b].append(a)
indegree[a] += 1
queue = deque(c for c in range(numCourses) if indegree[c] == 0)
order = []
while queue:
course = queue.popleft()
order.append(course)
for nxt in adj[course]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
return order if len(order) == numCourses else []
Walkthrough (numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]):
- Edges
0β1, 0β2, 1β3, 2β3. In-degrees 0:0, 1:1, 2:1, 3:2. Queue [0].
- Take 0 β
order=[0]; 1 and 2 drop to 0, enqueue β queue [1,2].
- Take 1 β
order=[0,1]; 3 drops to 1. Take 2 β order=[0,1,2]; 3 drops to 0, enqueue.
- Take 3 β
order=[0,1,2,3]. Length 4 = numCourses β return [0,1,2,3].
Complexity: O(V + E) time, O(V + E) space.
Approach 3 β DFS reverse post-order
The insight: in a DFS, a node is finished only after all nodes it points to are finished. So if you append each node at the moment it finishes (post-order) and then reverse the list, every edge u β v has u appearing before v β a topological order. Three-color state catches back edges (cycles) so you can abort with []. Prefer DFS when you already think recursively about the graph; prefer Kahnβs when recursion depth (up to 2000) is a concern.
class Solution:
def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]:
adj = [[] for _ in range(numCourses)]
for a, b in prerequisites:
adj[b].append(a)
WHITE, GRAY, BLACK = 0, 1, 2
state = [WHITE] * numCourses
post = []
def dfs(course: int) -> bool:
state[course] = GRAY
for nxt in adj[course]:
if state[nxt] == GRAY:
return False # back edge β cycle
if state[nxt] == WHITE and not dfs(nxt):
return False
state[course] = BLACK
post.append(course) # finished after all successors
return True
for c in range(numCourses):
if state[c] == WHITE and not dfs(c):
return []
post.reverse()
return post
Walkthrough (same 4-course example): DFS from 0 recurses 0β1β3 (3 finishes first: post=[3]), back up 1 finishes (post=[3,1]), then 0βs other edge to 2 β 3 already BLACK, 2 finishes (post=[3,1,2]), 0 finishes (post=[3,1,2,0]). Reversed β [0,2,1,3], a valid order.
Complexity: O(V + E) time, O(V + E) space.
Common pitfalls
- Forgetting to reverse the DFS post-order β the un-reversed list is the opposite of a valid order.
- Returning the partial
order on a cycle instead of []; always compare len(order) == numCourses first.
- Mixing up edge direction:
[a, b] is b β a. Reversed edges yield a reversed (invalid) topological sort.
- A
visited set that doesnβt separate GRAY from BLACK in DFS misses cycles or, worse, reports false ones.
Pattern takeaway
When a problem asks for an ordering consistent with βmust-come-beforeβ constraints, itβs a topological sort. Kahnβs BFS grows the answer as in-degrees reach 0; DFS reverse post-order builds it from finish times. Existence of any valid order equals acyclicity, so your cycle check is your impossibility check β return the sentinel when the count falls short.