MediumGraphs

Course Schedule โ€” Solution

Problem

Given a number of courses (labeled 0 through numCourses-1) and a list of prerequisite pairs, determine whether it's possible to complete all courses. Each pair [a, b] means course b must be finished before course a โ€” the question is whether these dependencies form a cycle that makes completion impossible.

  • Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
  • Output: true
  • Explanation: Take course 0 first, then 1 and 2 in either order, then 3.

Counter-example: numCourses = 2, prerequisites = [[0,1],[1,0]] โ†’ false, because course 0 requires 1 and course 1 requires 0 โ€” neither can ever start.

Intuition

The problem is equivalent to cycle detection in a directed graph: each pair [a, b] creates an edge b โ†’ a encoding "b precedes a." If any cycle exists, the courses involved all wait on each other and can never begin. A cycle-free directed graph (a DAG) always has a valid completion order.

Approach 1 โ€” BFS Topological Sort (Kahn's Algorithm)

Track how many unmet prerequisites each course has. Repeatedly "enroll" in any course whose count drops to zero โ€” if we can enroll in all courses, there was no cycle.

  1. Build an adjacency list and compute each course's in-degree (number of prerequisites).
  2. Seed a queue with every course that has zero prerequisites.
  3. While the queue is non-empty, dequeue a course and increment an enrollment counter.
  4. For each course that depends on the dequeued course, decrement its in-degree.
  5. If a dependent course's in-degree drops to zero, enqueue it.
  6. Return true if the enrollment counter equals numCourses.
1from collections import deque
2
3def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
4    adjacency = [[] for _ in range(numCourses)]
5    in_degree = [0] * numCourses
6
7    for course, prereq in prerequisites:
8        adjacency[prereq].append(course)  # prereq must come before course
9        in_degree[course] += 1
10
11    # start with courses that have no unmet prerequisites
12    queue = deque(c for c in range(numCourses) if in_degree[c] == 0)
13    enrolled = 0
14
15    while queue:
16        course = queue.popleft()
17        enrolled += 1
18        for next_course in adjacency[course]:
19            in_degree[next_course] -= 1
20            if in_degree[next_course] == 0:  # all prereqs now satisfied
21                queue.append(next_course)
22
23    # a cycle permanently keeps some courses at in_degree > 0
24    return enrolled == numCourses

Time: O(V+E) โ€” each course and each prerequisite pair is processed exactly once.
Space: O(V+E) โ€” adjacency list plus the in-degree array and BFS queue.

Approach 2 โ€” DFS with 3-State Coloring

Run DFS from every unvisited course, tagging each node as "currently on the call stack" while exploring it. If DFS reaches a node still on the stack, a back edge exists โ€” meaning a cycle.

  1. Build an adjacency list.
  2. Give each course state 0 (unvisited), 1 (active on current DFS path), or 2 (fully explored).
  3. For each unvisited course, run DFS.
  4. In DFS: state 1 means we've looped back to an ancestor โ€” return true (cycle found).
  5. State 2 means this subtree was already proven safe โ€” return false immediately.
  6. Set state to 1, recurse on all dependents, then set state to 2 before returning.
  7. Return false if no cycle was found across all DFS trees.
1def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
2    adjacency = [[] for _ in range(numCourses)]
3    for course, prereq in prerequisites:
4        adjacency[prereq].append(course)
5
6    # 0 = unvisited, 1 = on current DFS stack, 2 = fully explored (safe)
7    state = [0] * numCourses
8
9    def has_cycle(node: int) -> bool:
10        if state[node] == 1:  # back edge: this node is an ancestor on the current path
11            return True
12        if state[node] == 2:  # already proved no cycle exists past this node
13            return False
14        state[node] = 1
15        for neighbor in adjacency[node]:
16            if has_cycle(neighbor):
17                return True
18        state[node] = 2  # all descendants safe; mark as fully explored
19        return False
20
21    for node in range(numCourses):
22        if state[node] == 0 and has_cycle(node):
23            return False
24    return True

Time: O(V+E) โ€” each node and each edge is visited at most once.
Space: O(V+E) โ€” adjacency list, state array, and recursion stack up to depth V in the worst case.

Complexity Summary

ApproachTimeSpaceWhen to use
BFS Topological SortO(V+E)O(V+E)When you also need the valid course order or a count of reachable nodes
DFS 3-State ColoringO(V+E)O(V+E)When only a yes/no answer is needed; generalizes naturally to finding the cycle itself

Common Mistakes

  • Building edges in the wrong direction โ€” prerequisites[i] = [a, b] means b must precede a, so the graph edge goes from b to a. Reversing it (a โ†’ b) inverts all dependencies and silently produces wrong answers on almost every input.

  • Using only 2 DFS states (visited/unvisited) โ€” without a distinct "active" state, a node in a different DFS branch is indistinguishable from a node on the current path. Any cross edge gets misread as a back edge, producing false cycle reports.

  • Skipping disconnected components in the outer loop โ€” a course with no prerequisites and no dependents is never reached unless you explicitly start DFS or count it. Always iterate over all nodes in the outer loop, not just those that appear in the prerequisites list.

  • Checking enrolled == prerequisites.length instead of enrolled == numCourses in Kahn's algorithm โ€” a course that appears in no prerequisite pair still needs to be counted. The comparison must be against the total number of courses.

  • Not accounting for Python's recursion limit in the DFS approach โ€” with up to 2000 courses, a path-shaped graph can push 2000 recursive calls. For production code or very large inputs, convert the DFS to an explicit stack; for LeetCode's constraints it's typically fine.

Related Problems

  • Number of Provinces โ€” same union-find or DFS connected-component structure, just on an undirected graph
  • Clone Graph โ€” same adjacency-list graph traversal using DFS/BFS
  • All Paths From Source to Target โ€” DFS path enumeration on a DAG; course schedule is the step before (verifying it's a DAG at all)
  • Rotting Oranges โ€” multi-source BFS with a "count processed" termination condition, the same pattern as Kahn's enrollment counter
  • Find if Path Exists in Graph โ€” foundational graph reachability problem using the same BFS/DFS setup

Ready to practice? Try it on SkillFlow

Adaptive problems, AI follow-up interviews, and a skill score that shows exactly where you need to improve.

Practice This Problem โ†’