207. Course Schedule - Explanation
Description
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.
The pair [0, 1], indicates that must take course 1 before taking course 0.
There are a total of numCourses courses you are required to take, labeled from 0 to numCourses - 1.
Return true if it is possible to finish all courses, otherwise return false.
Example 1:
Input: numCourses = 2, prerequisites = [[0,1]]
Output: trueExplanation: First take course 1 (no prerequisites) and then take course 0.
Example 2:
Input: numCourses = 2, prerequisites = [[0,1],[1,0]]
Output: falseExplanation: In order to take course 1 you must take course 0, and to take course 0 you must take course 1. So it is impossible.
Constraints:
1 <= numCourses <= 10000 <= prerequisites.length <= 1000prerequisites[i].length == 20 <= a[i], b[i] < numCourses- All
prerequisitepairs are unique.
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(V + E) time and O(V + E) space, where V is the number of courses (nodes) and E is the number of prerequisites (edges).
Hint 1
Consider the problem as a graph where courses represent the nodes, and prerequisite[i] = [a, b] represents a directed edge from a to b. We need to determine whether the graph contains a cycle. Why? Because if there is a cycle, it is impossible to complete the courses involved in the cycle. Can you think of an algorithm to detect cycle in a graph?
Hint 2
We can use the Depth First Search (DFS) algorithm to detect a cycle in a graph. We iterate over each course, run a DFS from that course, and first try to finish its prerequisite courses by recursively traversing through them. To detect a cycle, we initialize a hash set called path, which contains the nodes visited in the current DFS call. If we encounter a course that is already in the path, we can conclude that a cycle is detected. How would you implement it?
Hint 3
We run a DFS starting from each course by initializing a hash set, path, to track the nodes in the current DFS call. At each step of the DFS, we return false if the current node is already in the path, indicating a cycle. We recursively traverse the neighbors of the current node, and if any of the neighbor DFS calls detect a cycle, we immediately return false. Additionally, we clear the neighbors list of a node when no cycle is found from that node to avoid revisiting those paths again.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Graph Representation - Building adjacency lists from edge pairs to represent directed graphs
- Depth-First Search (DFS) - Recursive graph traversal for exploring dependencies and detecting cycles
- Topological Sort (Kahn's Algorithm) - BFS-based approach using indegree to determine if a valid ordering exists
- Cycle Detection - Tracking nodes in the current recursion path to identify back edges indicating cycles
1. Cycle Detection (DFS)
Intuition
Each course is a node, and each prerequisite is a directed edge.
You can finish all courses only if there is no cycle in this directed graph.
A cycle means:
- Course A needs B
- B needs C
- C needs A
So you’re stuck forever.
We use DFS with cycle detection:
- While doing DFS, keep track of courses in the current recursion path.
- If we visit a course already in the current path → cycle found.
- If a course has no prerequisites left, it’s safe.
Algorithm
- Build a graph where each course points to its prerequisites.
- Use a
visitingset to track the current DFS path. - For each course:
- Run DFS.
- If the course is already in
visiting, returnfalse(cycle). - Recursively DFS its prerequisites.
- After successfully processing a course, clear its prerequisite list (mark as done).
- If all courses are processed without cycles, return
true.
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# 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
visiting = set()
def dfs(crs):
if crs in visiting:
# Cycle detected
return False
if preMap[crs] == []:
return True
visiting.add(crs)
for pre in preMap[crs]:
if not dfs(pre):
return False
visiting.remove(crs)
preMap[crs] = []
return True
for c in range(numCourses):
if not dfs(c):
return False
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of courses and is the number of prerequisites.
2. Topological Sort (Kahn's Algorithm)
Intuition
Treat each course as a node and each prerequisite as a directed edge.
If a course has no prerequisites, it can be taken immediately.
Kahn's Algorithm repeatedly takes courses that have zero prerequisites.
When we finish a course, we remove its dependency effect from other courses.
- If all courses can be taken this way - no cycle, return
true - If some courses are never taken - cycle exists, return
false
Algorithm
- Build a graph and compute
indegree(number of prerequisites) for each course. - Add all courses with
indegree = 0into a queue. - While the queue is not empty:
- Remove a course from the queue.
- Mark it as finished.
- Reduce the
indegreeof its dependent courses. - If any dependent course reaches
indegree = 0, add it to the queue.
- After processing:
- If finished courses == total courses - return
true - Else - return
false
- If finished courses == total courses - return
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
indegree = [0] * numCourses
adj = [[] for i in range(numCourses)]
for src, dst in prerequisites:
indegree[dst] += 1
adj[src].append(dst)
q = deque()
for n in range(numCourses):
if indegree[n] == 0:
q.append(n)
finish = 0
while q:
node = q.popleft()
finish += 1
for nei in adj[node]:
indegree[nei] -= 1
if indegree[nei] == 0:
q.append(nei)
return finish == numCoursesTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of courses and is the number of prerequisites.
Common Pitfalls
Using a Single Visited Set Instead of Tracking the Current Path
A single visited set marks nodes as seen globally, but cycle detection requires knowing if a node is in the current DFS path. Without tracking the recursion path separately, you may miss cycles or falsely detect them.
# Wrong: single visited set
visited = set()
def dfs(node):
if node in visited:
return False # This doesn't distinguish "finished" from "in current path"
visited.add(node)
for nei in adj[node]:
if not dfs(nei):
return False
return True
# Correct: track visiting (current path) and visited (finished)
visiting = set()
def dfs(node):
if node in visiting:
return False # Cycle: node is in current recursion path
if preMap[node] == []:
return True # Already processed, no cycle
visiting.add(node)
for nei in preMap[node]:
if not dfs(nei):
return False
visiting.remove(node)
preMap[node] = [] # Mark as processed
return TrueForgetting to Handle Disconnected Components
If you only start DFS from one node, you may miss cycles in disconnected parts of the graph. Always iterate through all courses and run DFS from each unvisited node.
# Wrong: only check from course 0
return dfs(0)
# Correct: check all courses
for c in range(numCourses):
if not dfs(c):
return False
return TrueIncorrectly Building the Adjacency List
The prerequisite [a, b] means "to take course a, you must first take course b". Mixing up which course maps to which leads to checking the wrong dependencies and missing or falsely reporting cycles.
Sign in to join the discussion