LeetCode 207 - Course Schedule
This problem models course dependencies as a directed graph. Each course is represented as a node, and each prerequisite relationship is represented as a directed edge. If prerequisites[i] = [a, b], that means course b must be completed before course a.
Difficulty: 🟡 Medium
Topics: Depth-First Search, Breadth-First Search, Graph Theory, Topological Sort
Solution
Problem Understanding
This problem models course dependencies as a directed graph. Each course is represented as a node, and each prerequisite relationship is represented as a directed edge.
If prerequisites[i] = [a, b], that means course b must be completed before course a. In graph terms, there is a directed edge from b to a.
The task is to determine whether it is possible to complete all courses. This is equivalent to asking whether the dependency graph contains a cycle.
A cycle means there is a circular dependency. For example:
- Course 0 requires Course 1
- Course 1 requires Course 0
In this situation, neither course can ever be taken first, so finishing all courses becomes impossible.
The input consists of:
numCourses, the total number of courses labeled from0tonumCourses - 1prerequisites, a list of prerequisite relationships
The expected output is:
trueif all courses can eventually be completedfalseif at least one cyclic dependency exists
The constraints are important because they guide the choice of algorithm:
- Up to 2000 courses
- Up to 5000 prerequisite relationships
A brute-force simulation would become inefficient as the graph grows. These constraints strongly suggest using graph traversal algorithms such as Depth-First Search (DFS), Breadth-First Search (BFS), or topological sorting.
Several edge cases are important:
- No prerequisites at all, every course can be completed immediately
- A direct cycle between two courses
- A longer cycle involving multiple courses
- Multiple disconnected groups of courses
- Courses that never appear in the prerequisite list
- A graph where many courses depend on the same prerequisite
The problem also guarantees that all prerequisite pairs are unique, so duplicate edges do not need special handling.
Approaches
Brute Force Approach
A naive approach would repeatedly search for courses whose prerequisites are already satisfied, remove them, and continue until either all courses are processed or no progress can be made.
Conceptually, this works as follows:
- Scan every course
- Check whether all of its prerequisites are completed
- If so, mark it as completed
- Repeat until no new courses can be completed
This approach is correct because it simulates the actual process of taking courses in valid order. If at some point no course can be taken while unfinished courses still remain, then a cycle must exist.
However, this repeated scanning becomes inefficient. In the worst case, every iteration may scan all courses and all prerequisite relationships again. With up to 2000 courses and 5000 edges, the repeated work becomes unnecessarily expensive.
Optimal Approach
The key observation is that this problem is fundamentally a cycle detection problem in a directed graph.
If the graph has no cycle, then a valid topological ordering exists, meaning courses can be completed in some order.
A very efficient way to detect cycles is to use topological sorting with Breadth-First Search, commonly known as Kahn's Algorithm.
The idea is:
- Count how many prerequisites each course has, this is called the indegree
- Any course with indegree 0 can be taken immediately
- Once a course is completed, remove its outgoing edges
- This may unlock additional courses whose indegree becomes 0
- Continue until no more courses can be processed
If all courses are processed, the graph is acyclic.
If some courses remain unprocessed, they must belong to a cycle.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(V × E) | O(V + E) | Repeatedly scans prerequisites to find available courses |
| Optimal | O(V + E) | O(V + E) | Uses BFS topological sort and indegree tracking |
Here, V is the number of courses and E is the number of prerequisite relationships.
Algorithm Walkthrough
Optimal Algorithm, Kahn's Topological Sort
- Build an adjacency list representing the graph.
For every prerequisite pair [a, b], add an edge from b to a.
This structure allows us to quickly find which courses become available after completing another course. 2. Create an indegree array.
indegree[i] stores how many prerequisites course i still requires before it can be taken.
Every time we add an edge b -> a, increment indegree[a].
3. Initialize a queue with all courses that have indegree 0.
These courses have no prerequisites and can be taken immediately.
A queue is ideal because we process courses in the order they become available. 4. Start processing the queue.
Remove one course from the queue and count it as completed.
Then look at all neighboring courses that depend on it. 5. Reduce the indegree of neighboring courses.
Completing the current course satisfies one prerequisite for each neighbor.
Therefore, decrement their indegree by 1. 6. Add newly unlocked courses to the queue.
If a neighbor's indegree becomes 0, it means all prerequisites are now satisfied.
Push it into the queue so it can be processed later. 7. Continue until the queue becomes empty.
At this point, either:
- All courses were processed successfully
- Some courses remain stuck in a cycle
- Compare the number of processed courses with
numCourses.
If they match, return True.
Otherwise, return False.
Why it works
The algorithm maintains the invariant that only courses with all prerequisites satisfied enter the queue.
In a graph without cycles, repeatedly removing indegree-0 nodes eventually processes every node. This is a fundamental property of Directed Acyclic Graphs, also called DAGs.
If a cycle exists, every node inside the cycle will always have at least one remaining prerequisite. Therefore, none of them will ever reach indegree 0, and the algorithm will terminate early with unprocessed courses remaining.
Python Solution
from collections import deque
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# Build adjacency list
graph = [[] for _ in range(numCourses)]
# indegree[i] = number of prerequisites for course i
indegree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
indegree[course] += 1
# Start with courses that have no prerequisites
queue = deque()
for course in range(numCourses):
if indegree[course] == 0:
queue.append(course)
completed_courses = 0
while queue:
current = queue.popleft()
completed_courses += 1
# Remove outgoing edges
for neighbor in graph[current]:
indegree[neighbor] -= 1
# If all prerequisites are satisfied
if indegree[neighbor] == 0:
queue.append(neighbor)
return completed_courses == numCourses
The implementation begins by constructing the graph as an adjacency list. Each index represents a course, and the stored list contains courses that depend on it.
The indegree array tracks how many prerequisites remain for each course. Every prerequisite relationship increases the indegree of the dependent course.
The queue stores all currently available courses, meaning courses whose indegree is 0.
The BFS loop processes courses one by one. When a course is completed, all outgoing edges are effectively removed by decrementing the indegree of neighboring courses.
Whenever a neighbor's indegree becomes 0, it means every prerequisite has been satisfied, so the course becomes eligible for processing.
Finally, the algorithm checks whether the number of completed courses equals the total number of courses. If not, some courses were trapped in a cycle.
Go Solution
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
indegree := make([]int, numCourses)
// Build graph and indegree array
for _, pair := range prerequisites {
course := pair[0]
prereq := pair[1]
graph[prereq] = append(graph[prereq], course)
indegree[course]++
}
// Queue for BFS
queue := []int{}
for course := 0; course < numCourses; course++ {
if indegree[course] == 0 {
queue = append(queue, course)
}
}
completedCourses := 0
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
completedCourses++
for _, neighbor := range graph[current] {
indegree[neighbor]--
if indegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
return completedCourses == numCourses
}
The Go implementation follows the same algorithmic structure as the Python solution.
Instead of Python's deque, Go uses a slice as a queue. Removing the front element is done using queue = queue[1:].
Go slices are dynamically sized, making them appropriate for adjacency lists and BFS queues.
No special handling for integer overflow is necessary because all values remain well within Go's int range under the problem constraints.
An empty slice in Go behaves similarly to an empty list in Python, so no special nil handling is required.
Worked Examples
Example 1
Input:
numCourses = 2
prerequisites = [[1,0]]
Graph representation:
0 -> 1
Initial state:
| Course | Indegree |
|---|---|
| 0 | 0 |
| 1 | 1 |
Initial queue:
[0]
Processing steps:
| Step | Current Course | Queue After Processing | Indegree State | Completed |
|---|---|---|---|---|
| 1 | 0 | [1] | [0, 0] | 1 |
| 2 | 1 | [] | [0, 0] | 2 |
At the end:
completed_courses = 2
numCourses = 2
Since all courses were processed, the answer is true.
Example 2
Input:
numCourses = 2
prerequisites = [[1,0],[0,1]]
Graph representation:
0 -> 1
1 -> 0
Initial state:
| Course | Indegree |
|---|---|
| 0 | 1 |
| 1 | 1 |
Initial queue:
[]
No course has indegree 0, so the queue starts empty.
Processing steps:
| Step | Current Course | Queue | Completed |
|---|---|---|---|
| None | None | [] | 0 |
At the end:
completed_courses = 0
numCourses = 2
Not all courses could be processed, meaning a cycle exists.
The answer is false.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(V + E) | Each course and prerequisite edge is processed once |
| Space | O(V + E) | The adjacency list, indegree array, and queue require additional storage |
The graph construction phase processes every prerequisite once. The BFS traversal also visits every node and edge at most once.
The adjacency list stores all edges, the indegree array stores one value per course, and the queue can contain up to all courses in the worst case.
Test Cases
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
from collections import deque
graph = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
indegree[course] += 1
queue = deque()
for course in range(numCourses):
if indegree[course] == 0:
queue.append(course)
completed = 0
while queue:
current = queue.popleft()
completed += 1
for neighbor in graph[current]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return completed == numCourses
solution = Solution()
assert solution.canFinish(2, [[1, 0]]) == True # Simple valid dependency
assert solution.canFinish(2, [[1, 0], [0, 1]]) == False # Direct cycle
assert solution.canFinish(1, []) == True # Single course, no prerequisites
assert solution.canFinish(3, [[1, 0], [2, 1]]) == True # Linear dependency chain
assert solution.canFinish(3, [[1, 0], [2, 1], [0, 2]]) == False # Three-node cycle
assert solution.canFinish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]) == True # Multiple dependencies
assert solution.canFinish(5, []) == True # No prerequisites at all
assert solution.canFinish(4, [[1, 0], [2, 1]]) == True # Disconnected graph
assert solution.canFinish(6, [[1, 0], [2, 1], [3, 2], [1, 3]]) == False # Larger cycle
assert solution.canFinish(2000, []) == True # Maximum courses, no dependencies
| Test | Why |
|---|---|
2, [[1,0]] |
Basic valid dependency |
2, [[1,0],[0,1]] |
Smallest possible cycle |
1, [] |
Minimum input size |
3, [[1,0],[2,1]] |
Simple chain structure |
3, [[1,0],[2,1],[0,2]] |
Multi-node cycle detection |
4, [[1,0],[2,0],[3,1],[3,2]] |
Multiple prerequisites converging |
5, [] |
Entirely disconnected graph |
4, [[1,0],[2,1]] |
Some courses unused in prerequisites |
6, [[1,0],[2,1],[3,2],[1,3]] |
Larger cyclic structure |
2000, [] |
Upper boundary for course count |
Edge Cases
No prerequisites
A common edge case is when prerequisites is empty.
In this scenario, every course has indegree 0 and can immediately enter the queue. The algorithm correctly processes all courses and returns True.
Naive implementations sometimes forget to initialize isolated nodes properly, but this solution explicitly iterates through all courses when building the initial queue.
Cycles involving multiple courses
Cycles are not always direct two-course loops. A longer cycle such as:
0 -> 1 -> 2 -> 0
can be harder to detect with simplistic logic.
This implementation handles such cases naturally because none of the nodes in the cycle can ever reach indegree 0. As a result, the BFS traversal stops early, leaving unprocessed nodes behind.
Disconnected graph components
The graph may contain several completely separate groups of courses.
For example:
0 -> 1
2 -> 3
A buggy implementation might only traverse from one starting point and miss another component entirely.
This solution avoids that issue by initially inserting every indegree-0 course into the queue, ensuring all disconnected components are explored.
Courses not appearing in prerequisites
Some courses may never appear in the prerequisite list at all.
For example:
numCourses = 5
prerequisites = [[1,0]]
Courses 2, 3, and 4 are completely independent.
The algorithm handles this correctly because every course starts with indegree 0 unless explicitly assigned prerequisites. Independent courses are therefore processed normally.