Course Schedule

Medium DFS

Problem

Given the total number of courses and a list of prerequisite pairs, determine if it is possible to finish all courses by taking them in an order that satisfies all prerequisites.

  • 1 ≤ numCourses ≤ 10⁵
  • 0 ≤ prerequisites.length ≤ 5000
  • prerequisites[i].length == 2
  • 0 ≤ a_i, b_i < numCourses
  • All the pairs prerequisites[i] are unique.

Example

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

There are two courses: 0 and 1. To take course 1, you must first take course 0. The order [0,1] satisfies the prerequisites, so it is possible to finish all courses.

Approach

Straightforward Solution

A brute-force approach might try all permutations of courses to check if prerequisites are satisfied, which is factorial in complexity and infeasible for large inputs.

Core Observation

The problem reduces to detecting if a directed graph (courses as nodes, prerequisites as edges) contains a cycle. If a cycle exists, it is impossible to complete all courses because some courses depend on each other circularly.

Path to Optimal

Recognizing the problem as cycle detection in a directed graph leads to using DFS with a recursion stack (visit set) to detect back edges indicating cycles. Constructing an adjacency list from prerequisites allows efficient traversal. The DFS explores each course's prerequisites recursively, marking nodes in the current path. If a node is revisited in the current path, a cycle is detected. Memoization by clearing prerequisites after processing prevents redundant work.

Optimal Approach

Build an adjacency list mapping each course to its prerequisites. For each course, perform a DFS to check for cycles by tracking nodes in the current recursion stack. If a cycle is found, return false immediately. If no cycles are detected after processing all courses, return true. Clearing the prerequisites list after processing a course marks it as safe and avoids repeated DFS calls.

Time

O(V + E)

Each course (vertex) is visited once, and each prerequisite (edge) is explored once during DFS. Memoization by clearing prerequisites ensures no repeated work, resulting in linear time relative to the graph size.

Space

O(V + E)

The adjacency list stores all edges, requiring O(E) space. The recursion stack and visit set use O(V) space in the worst case, where V is the number of courses.

Pattern Spotlight

DFS (Cycle Detection in Directed Graph)

Use a recursion stack (visit set) to detect cycles by tracking nodes currently in the path; revisiting a node in this stack signals a cycle, enabling early termination.

Solution

1class Solution:
2 def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool:
3 pre_map = {i: [] for i in range(numCourses)}
4 for crs, pre in prerequisites:
5 pre_map[crs].append(pre)
6
7 visit_set = set()
8 def dfs(crs):
9 if crs in visit_set:
10 return False
11 if pre_map[crs] == []:
12 return True
13
14 visit_set.add(crs)
15 for pre in pre_map[crs]:
16 if not dfs(pre): return False
17 visit_set.remove(crs)
18 pre_map[crs] = []
19 return True
20
21 for crs in range(numCourses):
22 if not dfs(crs): return False
23 return True

Step-by-Step Solution

1

Build Adjacency List Mapping Courses to Prerequisites

3pre_map = {i: [] for i in range(numCourses)}
4for crs, pre in prerequisites:
5 pre_map[crs].append(pre)

Objective

To represent the course prerequisites as a graph for efficient traversal.

Key Insight

An adjacency list is a natural and efficient way to represent directed graphs, especially when edges are sparse. Mapping each course to its list of prerequisites allows quick access to dependencies during DFS. This structure transforms the problem into graph traversal and cycle detection.

Interview Quick-Check

Core Logic

The adjacency list stores all prerequisite edges, enabling O(1) access to a course's prerequisites during DFS.

Common Pitfalls & Bugs

Forgetting to initialize all courses in the adjacency list can cause key errors or missed nodes during traversal.

2

Use DFS with Recursion Stack to Detect Cycles

7visit_set = set()
8def dfs(crs):
9 if crs in visit_set:
10 return False
11 if pre_map[crs] == []:
12 return True
14 visit_set.add(crs)
15 for pre in pre_map[crs]:
16 if not dfs(pre): return False
17 visit_set.remove(crs)
18 pre_map[crs] = []
19 return True

Objective

To recursively explore prerequisites and detect cycles by tracking the current path.

Key Insight

DFS explores each course's prerequisites depth-first. The recursion stack (visit_set) tracks nodes currently being explored. If a node is encountered that is already in this stack, it indicates a cycle. Clearing the prerequisites list after processing a course marks it as safe, preventing redundant DFS calls and improving efficiency.

Interview Quick-Check

Core Logic

The recursion stack detects cycles by identifying back edges, and memoization by clearing prerequisites avoids repeated work.

State & Boundaries

Base cases: returning False immediately on cycle detection, and True when a course has no prerequisites left.

Common Pitfalls & Bugs

Failing to remove a course from the recursion stack after DFS exploration leads to false cycle detection or infinite recursion.

3

Iterate Over All Courses to Verify Completion Feasibility

21for crs in range(numCourses):
22 if not dfs(crs): return False
23return True

Objective

To ensure all courses are checked for cycles and dependencies.

Key Insight

Since the graph may be disconnected, each course must be checked. If any DFS call detects a cycle, the function returns False immediately. If all courses are processed without cycles, the function returns True, confirming all courses can be completed.

Interview Quick-Check

Core Logic

Iterating over all courses guarantees that disconnected components and isolated nodes are also checked for cycles.

Complexity

Early termination on cycle detection improves average runtime by avoiding unnecessary DFS calls.

Line Analysis

This solution has 9 Critical lines interviewers watch for.

Line 9 Critical
if crs in visit_set:

Check if the current course is already in the recursion stack.

Detecting a course in the current path signals a cycle, which invalidates the course completion order.

Line 17 Critical
visit_set.remove(crs)

Remove the current course from the recursion stack after exploration.

Removing the course restores state for other DFS paths and prevents false cycle detection in separate branches.

Line 7 Critical
visit_set = set()

Initialize a set to track courses currently in the recursion stack.

The visit_set tracks the current DFS path to detect cycles by identifying back edges, which is critical for correctness.

Line 10 Critical
return False

Return False immediately upon cycle detection.

Early termination on cycle detection prevents unnecessary further exploration and correctly signals impossibility.

Line 14 Critical
visit_set.add(crs)

Add the current course to the recursion stack.

Marking the course as being explored enables cycle detection by tracking the current DFS path.

Line 15 Critical
for pre in pre_map[crs]:

Recursively DFS on each prerequisite of the current course.

Exploring all prerequisites ensures that dependencies are validated and cycles are detected in the entire prerequisite chain.

Line 16 Critical
if not dfs(pre): return False

Return False immediately if any prerequisite DFS detects a cycle.

Propagating cycle detection up the recursion stack ensures the entire function returns False if any cycle exists.

Line 18 Critical
pre_map[crs] = []

Clear the prerequisites list for the current course to memoize safe completion.

Memoization by clearing prerequisites marks the course as processed and safe, avoiding redundant DFS calls and improving efficiency.

Line 22 Critical
if not dfs(crs): return False

Return False immediately if DFS detects a cycle for any course.

Early termination on cycle detection prevents unnecessary checks and correctly signals impossibility to complete all courses.

Line 3 Core
pre_map = {i: [] for i in range(numCourses)}

Initialize adjacency list mapping each course to an empty list of prerequisites.

Creating a complete adjacency list upfront ensures every course is represented, preventing key errors and enabling uniform graph traversal.

Line 5 Core
pre_map[crs].append(pre)

Append the prerequisite course to the current course's adjacency list.

Adding edges to the adjacency list encodes the prerequisite relationship, which is essential for detecting cycles and ordering courses.

Line 11 Core
if pre_map[crs] == []:

Check if the current course has no prerequisites left.

A course with no prerequisites is safe to complete, serving as a base case for recursion.

Line 19 Core
return True

Return True indicating the current course can be completed safely.

Returning True signals successful DFS traversal without cycles for this course and its prerequisites.

Line 21 Core
for crs in range(numCourses):

Iterate over all courses to check for cycles and completion feasibility.

Checking every course ensures disconnected components and isolated nodes are validated, guaranteeing correctness.

Line 23 Core
return True

Return True if all courses can be completed without cycles.

Returning True confirms that the entire course graph is acyclic and all courses can be finished in some order.

Line 4 Core
for crs, pre in prerequisites:

Iterate over each prerequisite pair to populate the adjacency list.

This loop builds the graph edges by associating each course with its prerequisites, forming the dependency structure needed for DFS.

Line 12 Core
return True

Return True when no prerequisites remain, indicating safe completion.

This base case terminates recursion successfully when a course is free of dependencies.

Line 8 Standard
def dfs(crs):

Define the DFS function to explore prerequisites of a given course.

Encapsulating the recursive logic in a helper function organizes the cycle detection process and manages state cleanly.

Test Your Understanding

Why does detecting a node already in the recursion stack during DFS indicate a cycle?

Because revisiting a node in the current recursion path means there is a back edge, forming a cycle where a course depends on itself indirectly, making it impossible to complete.

Related Problems

DFS pattern

Don't just read it. Drill it.

Reconstruct Course Schedule from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Course Schedule drill

or drill a free problem