DSAPrep
MediumGraphs

Course Schedule

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.

For example, the pair [0, 1] indicates that to take course 0 you have to first take course 1.

Return true if you can finish all courses. Otherwise, return false.

Example 1

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are 2 courses. To take course 1 you should have finished course 0. So it is possible.

Example 2

Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: To take course 1 you need course 0 finished, and to take course 0 you need course 1 finished. Neither can go first, so it is impossible.

Constraints

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • All the pairs prerequisites[i] are unique.
View original on LeetCode β†—

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:

  1. dfs(0): mark 0 VISITING. No dependencies. Mark 0 DONE. Returns True.
  2. dfs(1): mark 1 VISITING. Recurse into dependency 0 (DONE, returns True immediately). Recurse into dependency 3.
  3. dfs(3): mark 3 VISITING. Recurse into dependency 2.
  4. dfs(2): mark 2 VISITING. Recurse into dependency 1 β€” but 1 is currently VISITING (it’s the caller three frames up!). This is a back edge: dfs(1) returns False immediately, which propagates back through dfs(2), dfs(3), and dfs(1), and the overall function returns False.

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 == numCourses

Watch 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)

00
10

Prerequisite edges (left unlocks right)

0unlocks1

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

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

Result order β€” 0 / 2 courses scheduled

none yet β€” nothing has finished
1 / 6
ready β€” no prerequisites leftin-degree is changingscheduled β€” in the result ordertrapped in a cycle

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)

00
11
20
30

Prerequisite edges (left unlocks right)

0unlocks11unlocks22unlocks33unlocks1

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

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

Result order β€” 0 / 4 courses scheduled

none yet β€” nothing has finished
1 / 7
ready β€” no prerequisites leftin-degree is changingscheduled β€” in the result ordertrapped in a cycle

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.