DSAPrep
MediumGraphs

Course Schedule II

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.

Example 1

Input: numCourses = 2, prerequisites = [[1,0]]
Output: [0,1]
Explanation: To take course 1 you should have finished course 0. The correct order is [0,1].

Example 2

Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0,2,1,3]
Explanation: Course 3 needs both 1 and 2; both 1 and 2 need 0. One correct order is [0,1,2,3]; another is [0,2,1,3].

Example 3

Input: numCourses = 1, prerequisites = []
Output: [0]

Constraints

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • All the pairs [ai, bi] are distinct.
View original on LeetCode β†—

Same graph as Course Schedule β€” courses are nodes, [a, b] means edge a -> b (β€œa depends on b”) β€” but now the goal is to actually produce a valid order, not just detect whether one exists. That is precisely a topological sort: an ordering of nodes such that every edge a -> b places b before a. If the graph has a cycle, no such ordering exists and the answer is [].

Running example: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] (course 1 needs 0; course 2 needs 0; course 3 needs both 1 and 2).

Course Depends on
0 (none)
1 0
2 0
3 1, 2

DFS Post-Order Topological Sort

Time O(V + E)Space O(V + E)

Run the same cycle-detecting DFS as Course Schedule, but this time append each course to an order list right after all of its dependencies have finished processing. Because a course is only appended once every course it depends on is already in order, the list comes out in a valid dependency order directly (no reversal needed).

class Solution:
def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]:
graph = [[] for _ in range(numCourses)]
for a, b in prerequisites:
graph[a].append(b)
UNVISITED, VISITING, DONE = 0, 1, 2
state = [UNVISITED] * numCourses
order = []
def dfs(u):
if state[u] == VISITING:
return False
if state[u] == DONE:
return True
state[u] = VISITING
for v in graph[u]:
if not dfs(v):
return False
state[u] = DONE
order.append(u)
return True
for u in range(numCourses):
if not dfs(u):
return []
return order

Tracing on the example, looping dfs(0) through dfs(3):

  1. dfs(0): no dependencies. Append 0. order = [0].
  2. dfs(1): depends on 0, already DONE. Append 1. order = [0, 1].
  3. dfs(2): depends on 0, already DONE. Append 2. order = [0, 1, 2].
  4. dfs(3): depends on 1 and 2, both already DONE. Append 3. order = [0, 1, 2, 3].

Why it’s correct: a node is appended to order only after every node it depends on is already in order (either appended earlier or confirmed DONE in this call), so by induction every dependency precedes its dependent in the final list. Complexity: each node is visited once and each edge is followed once β†’ O(V + E) time, O(V + E) space for the graph, color array, and recursion stack.

Kahn's Algorithm (BFS Topological Sort)

OptimalTime O(V + E)Space O(V + E)

Build the graph pointing prerequisite β†’ dependent, track in-degrees (number of unmet prerequisites), and repeatedly dequeue courses with in-degree 0 β€” courses that are ready to take right now β€” appending each to the result as it’s dequeued.

from collections import deque
class Solution:
def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]:
graph = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
indegree[a] += 1
queue = deque(u for u in range(numCourses) if indegree[u] == 0)
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
queue.append(v)
return order if len(order) == numCourses else []

Play through the same example β€” n = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]. Watch the in-degree badges feed the ready queue, and the queue feed the order below β€” every pop is one course locked into the answer:

Courses(badge = prerequisites still needed)

00
10
20
30

Prerequisite edges (left unlocks right)

0unlocks10unlocks21unlocks32unlocks3

Queue β€” front on the left, next to pop

empty β€” no course has in-degree 0 yet

Order so far β€” 0 / 4 courses popped

none yet β€” nothing has been popped
1 / 11
ready β€” no prerequisites left, waiting in the queuein-degree is changingscheduled β€” pop sequence so fartrapped in a cycle β€” never ready

Counting phase: each prerequisite pair adds one point to the in-degree of the course that depends on it β€” the badges that will tell us how many prerequisites a course still needs. The pairs are [1,0], [2,0], [3,1], [3,2]: course 1 needs 0, course 2 needs 0, course 3 needs both 1 and 2. The chips above show the same edges in unlock direction, so watch course 3 (hit by two pairs) climb the furthest.

A cycle flips the answer to [] β€” a queue that runs dry without finishing every course is a queue that got stuck. On n = 3, prerequisites = [[1,0],[2,1],[1,2]], courses 1 and 2 wait on each other. Watch the queue seed, pop course 0, and then die:

Courses(badge = prerequisites still needed)

00
10
20

Prerequisite edges (left unlocks right)

0unlocks11unlocks22unlocks1

Queue β€” front on the left, next to pop

empty β€” no course has in-degree 0 yet

Order so far β€” 0 / 3 courses popped

none yet β€” nothing has been popped
1 / 6
ready β€” no prerequisites left, waiting in the queuein-degree is changingscheduled β€” pop sequence so fartrapped in a cycle β€” never ready

Same algorithm, now on a cyclic graph: 3 courses with pairs [1,0], [2,1], and [1,2]. Pair [1,0]: course 1 needs course 0, so indegree[1] becomes 1.

Why it’s correct: a course reaches in-degree 0 exactly when every course it depends on has already been dequeued (placed earlier in order), so appending it at that moment preserves every dependency constraint. If a cycle exists, every course in it keeps at least one un-decremented in-degree forever, so order never reaches length numCourses, correctly signaling impossibility. Complexity: each node is enqueued once and each edge decrements one counter β†’ O(V + E) time, O(V + E) space. Same complexity as the DFS version, but iterative β€” no recursion depth to worry about on graphs with long chains, and the ready-to-take intuition maps directly onto how you’d actually plan the courses.