Leetcode 207 - Course Schedule
Understanding the Problem
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] = [a, b] indicates that you must take course b first if you want to take course a.
Return:
Trueif you can finish all courses.Falseif it is impossible (i.e., a dependency cycle exists).
Key Data Structures
1. Adjacency List (preMap)
A hash map mapping each course to a list of its required prerequisites. This enables quick \(O(1)\) lookup for outgoing edges during graph traversal.
2. Recursion Stack Set (visiting)
A set used during Depth-First Search (DFS) to track courses currently on the active recursion path. If a node is re-visited while still in this set, a cycle is detected.
Optimal Approach Intuition
This problem can be modeled as finding a directed cycle in a Directed Graph:
- Nodes: Courses (\(0\) to
numCourses - 1). - Directed Edges: Prerequisites (\(a \to b\)).
If the graph contains a directed cycle (e.g., Course A requires B, B requires C, C requires A), it is impossible to complete all courses. If the graph is a Directed Acyclic Graph (DAG), a valid topological ordering exists.
Why DFS with Backtracking and Memoization?
1. Backtracking for Cycle Detection
Adding a course to visiting before traversing deeper and removing it upon return allows us to track nodes along the current traversal path. If DFS encounters a course already in visiting, a back-edge (cycle) has been hit.
2. Memoization via Graph Pruning
Once a course is verified as reachable and cycle-free (returns True), we clear its prerequisites list (preMap[crs] = []). If another traversal path reaches this course later, it returns True immediately in \(O(1)\) time without re-exploring downstream paths.
How the Optimal Approach Works
Step 1: Build the Adjacency Map
Map each course to its list of prerequisites.
Step 2: DFS Traversal with Cycle Check
Iterate through all courses from \(0\) to numCourses - 1. Perform DFS on each course to verify no cycles exist:
- Base Case 1: If
crs in visiting, a cycle is detected \(\to\) ReturnFalse. - Base Case 2: If
preMap[crs] == [], the course has no prerequisites (or was already fully validated) \(\to\) ReturnTrue. - Add
crstovisiting. - Recursively run DFS on all prerequisites of
crs. If any returnFalse, propagateFalse. - Remove
crsfromvisiting(backtrack). - Set
preMap[crs] = []to cache the result. - Return
True.
Solution Implementation
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# Step 1: Map each course to its prerequisites
preMap = {i: [] for i in range(numCourses)}
for crs, pre in prerequisites:
preMap[crs].append(pre)
# Store all courses along the current DFS path (cycle detection)
visiting = set()
def dfs(crs):
# Base Case 1: Cycle detected
if crs in visiting:
return False
# Base Case 2: Fully validated or no prerequisites
if preMap[crs] == []:
return True
visiting.add(crs)
# Check all dependencies
for pre in preMap[crs]:
if not dfs(pre):
return False
# Backtrack and memoize
visiting.remove(crs)
preMap[crs] = []
return True
# Step 2: Run DFS for each course (handles disconnected components)
for c in range(numCourses):
if not dfs(c):
return False
return True
Complexity Analysis
Let:
- \(V\) =
numCourses(Number of vertices) - \(E\) =
len(prerequisites)(Number of edges)
Time Complexity: \(O(V + E)\)
- Adjacency Map Construction: \(O(V + E)\) to build
preMap. - DFS Traversal: Each node is visited once and each edge is traversed at most once because nodes are marked processed (
preMap[crs] = []) after validation.
Overall Time Complexity: \(O(V + E)\)
Space Complexity: \(O(V + E)\)
- Adjacency Map (
preMap): Stores \(V\) keys and \(E\) total prerequisite entries \(\to O(V + E)\). - Recursion Stack &
visitingset: Can grow up to \(O(V)\) depth in the worst-case linear dependency graph.
Overall Space Complexity: \(O(V + E)\)
Key Takeaways
- Cycle Detection Pattern: A cycle exists in a directed graph if traversal hits a node currently residing on the active recursion stack (
visitingset). - Pruning / Memoization: Resetting processed node edges (
preMap[crs] = []) prevents re-exploring shared branches across DAG components, reducing runtime to linear \(O(V + E)\). - Handling Disconnected Graphs: An outer loop across \(0 \dots V-1\) ensures isolated nodes and independent components are evaluated.
Common Mistakes
Mistake 1: Confusing Global Visited with Path Visited
Using a global visited set without backtracking elements upon exit falsely identifies cycles on converging non-cyclic paths (e.g., \(A \to C\) and \(B \to C\)).
Fix:
- Remove
crsfromvisitingwhen returning from the recursive step.
Mistake 2: Missing the Memoization Step
Forgetting to prune preMap[crs] = [] upon completion forces DFS to re-explore shared dependencies repeatedly, leading to TLE (Time Limit Exceeded).
Fix:
- Clear the node's list of dependencies once all sub-paths return
True.
Mistake 3: Assuming Graph Connectivity
Running DFS starting only from vertex 0 assumes all courses are reachable from 0.
Fix:
- Iterate through every course from
0tonumCourses - 1in the outer loop.