207. Course Schedule
Problem Description
You need to take numCourses
courses in total, numbered from 0
to numCourses - 1
. Some courses have prerequisites that must be completed before you can take them.
You're given an array prerequisites
where each element prerequisites[i] = [ai, bi]
means that you must complete course bi
before taking course ai
. For instance, [0, 1]
means you need to complete course 1
before you can take course 0
.
The task is to determine whether it's possible to complete all courses given these prerequisite constraints. Return true
if you can finish all courses, and false
if it's impossible.
This problem essentially asks you to detect if there's a circular dependency in the course prerequisites. If course A requires course B, and course B requires course C, and course C requires course A, then it would be impossible to complete all courses. The solution uses topological sorting to detect such cycles by:
- Building a graph where edges represent prerequisite relationships
- Tracking the in-degree (number of prerequisites) for each course
- Starting with courses that have no prerequisites (in-degree of 0)
- Processing courses level by level, reducing in-degrees of dependent courses
- Checking if all courses can be processed (no cycles exist)
If we can process all numCourses
courses through this method, it means there are no circular dependencies and all courses can be completed.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves courses as nodes and prerequisites as directed edges between courses. Each prerequisite
[a, b]
represents a directed edge from courseb
to coursea
.
Is it a tree?
- No: The graph can have multiple paths between nodes and may contain cycles. A course can have multiple prerequisites, and a course can be a prerequisite for multiple other courses.
Is the problem related to directed acyclic graphs?
- Yes: We need to determine if the course dependency graph is acyclic. If there's a cycle in the prerequisites (e.g., course A requires B, B requires C, and C requires A), then it's impossible to complete all courses. The problem is essentially asking whether the directed graph is acyclic (DAG).
Topological Sort
- Yes: The flowchart leads us to Topological Sort, which is perfect for this problem.
Conclusion: The flowchart suggests using Topological Sort for the Course Schedule problem. While the solution shown uses BFS-based topological sorting (Kahn's algorithm), DFS can also be used to perform topological sorting by detecting cycles during the traversal. In DFS approach, we would mark nodes as visiting/visited and if we encounter a node that's currently being visited (in the current DFS path), we've found a cycle.
Intuition
The key insight is to recognize that course prerequisites form a directed graph where each course is a node and each prerequisite relationship is a directed edge. When course b
is a prerequisite for course a
, we draw an edge from b
to a
.
The core question becomes: can we find an order to take all courses while respecting prerequisites? This is only possible if there are no circular dependencies. Imagine if course A requires B, B requires C, and C requires A - we'd be stuck in a loop with no starting point.
This circular dependency is exactly what we call a cycle in graph theory. So the problem reduces to: does our directed graph contain a cycle?
To detect cycles, we can use the concept of topological sorting. The idea is simple: start with courses that have no prerequisites (in-degree of 0). These can be taken immediately. After "taking" these courses, we remove them from consideration and see which new courses now have all their prerequisites satisfied.
Think of it like peeling an onion layer by layer:
- First layer: courses with no prerequisites
- Second layer: courses whose only prerequisites were in the first layer
- Third layer: courses whose prerequisites were all in the first two layers
- And so on...
If we can successfully peel away all layers (process all courses), then there's no cycle and we can complete everything. But if at some point we can't find any course with all prerequisites satisfied (all remaining courses are waiting for each other), then we've detected a cycle.
The algorithm tracks this by counting how many prerequisites each course has (indeg
). As we "complete" courses, we reduce this count for dependent courses. If a course's count reaches 0, it's ready to be taken. If we process all numCourses
this way, we return true
; otherwise, there's a cycle and we return false
.
Solution Approach
Following the topological sorting approach, let's walk through the implementation step by step:
1. Build the Graph and Track In-degrees
First, we create an adjacency list representation of the graph and count the in-degrees (number of prerequisites) for each course:
g = [[] for _ in range(numCourses)] # Adjacency list
indeg = [0] * numCourses # In-degree array
for a, b in prerequisites:
g[b].append(a) # b -> a (course b is prerequisite for course a)
indeg[a] += 1 # Course a has one more prerequisite
2. Find Starting Points
We identify all courses with no prerequisites (in-degree of 0) as our starting points:
q = [i for i, x in enumerate(indeg) if x == 0]
These courses can be taken immediately without any prerequisites.
3. Process Courses Layer by Layer
We process courses using BFS-like traversal (though we're using a list instead of a queue for simplicity):
for i in q: numCourses -= 1 # Mark this course as completed for j in g[i]: # For each course that depends on course i indeg[j] -= 1 # Reduce its prerequisite count if indeg[j] == 0: # If all prerequisites are satisfied q.append(j) # Add it to be processed
The key insight here is that we're using the list q
dynamically - as we iterate through it, we may append new elements, and the loop continues until we've processed all reachable courses.
4. Check if All Courses Were Processed
Finally, we check if we've successfully processed all courses:
return numCourses == 0
If numCourses
reaches 0, it means we've successfully found an order to take all courses. If not, some courses remain unprocessed due to circular dependencies.
Time Complexity: O(V + E)
where V is the number of courses and E is the number of prerequisites, as we visit each node once and traverse each edge once.
Space Complexity: O(V + E)
for storing the graph and in-degree array.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Example: numCourses = 4
, prerequisites = [[1,0], [2,0], [3,1], [3,2]]
This means:
- Course 1 requires course 0
- Course 2 requires course 0
- Course 3 requires course 1
- Course 3 requires course 2
Step 1: Build Graph and Calculate In-degrees
We create the adjacency list and count prerequisites:
g[0] = [1, 2]
(course 0 is prerequisite for courses 1 and 2)g[1] = [3]
(course 1 is prerequisite for course 3)g[2] = [3]
(course 2 is prerequisite for course 3)g[3] = []
(course 3 is not a prerequisite for any course)
In-degrees:
indeg[0] = 0
(no prerequisites)indeg[1] = 1
(requires course 0)indeg[2] = 1
(requires course 0)indeg[3] = 2
(requires courses 1 and 2)
Step 2: Find Starting Points
Course 0 has in-degree of 0, so q = [0]
Step 3: Process Courses Layer by Layer
Processing course 0:
- Decrement
numCourses
to 3 - Check dependent courses: 1 and 2
indeg[1]
becomes 0 β add course 1 to qindeg[2]
becomes 0 β add course 2 to q
- Now
q = [0, 1, 2]
Processing course 1:
- Decrement
numCourses
to 2 - Check dependent course: 3
indeg[3]
becomes 1 (still has course 2 as prerequisite)
q = [0, 1, 2]
(no new additions)
Processing course 2:
- Decrement
numCourses
to 1 - Check dependent course: 3
indeg[3]
becomes 0 β add course 3 to q
- Now
q = [0, 1, 2, 3]
Processing course 3:
- Decrement
numCourses
to 0 - No dependent courses
Step 4: Check Result
numCourses == 0
, so we return true
. All courses can be completed in the order: 0 β 1 β 2 β 3 (or 0 β 2 β 1 β 3).
Counter-example with a cycle:
If we had prerequisites = [[1,0], [0,1]]
(course 1 requires 0, and course 0 requires 1):
- Both courses would have in-degree of 1
q
would be empty initially- No courses could be processed
numCourses
would remain 2, returningfalse
Solution Implementation
1class Solution:
2 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
3 # Build adjacency list graph where graph[i] contains courses that depend on course i
4 graph = [[] for _ in range(numCourses)]
5
6 # Track the number of prerequisites (in-degree) for each course
7 in_degree = [0] * numCourses
8
9 # Build the graph and calculate in-degrees
10 # For prerequisite [a, b]: course a depends on course b (must take b before a)
11 for course, prerequisite in prerequisites:
12 graph[prerequisite].append(course)
13 in_degree[course] += 1
14
15 # Initialize queue with all courses that have no prerequisites (in-degree = 0)
16 queue = [course for course, degree in enumerate(in_degree) if degree == 0]
17
18 # Process courses using topological sort (Kahn's algorithm)
19 for current_course in queue:
20 # Decrement the count of remaining courses to process
21 numCourses -= 1
22
23 # For each course that depends on the current course
24 for dependent_course in graph[current_course]:
25 # Remove the prerequisite edge
26 in_degree[dependent_course] -= 1
27
28 # If all prerequisites are satisfied, add to queue
29 if in_degree[dependent_course] == 0:
30 queue.append(dependent_course)
31
32 # If all courses were processed, there's no cycle and courses can be finished
33 return numCourses == 0
34
1class Solution {
2 public boolean canFinish(int numCourses, int[][] prerequisites) {
3 // Create adjacency list to represent the course dependency graph
4 // graph[i] contains all courses that depend on course i
5 List<Integer>[] graph = new List[numCourses];
6 Arrays.setAll(graph, index -> new ArrayList<>());
7
8 // Track the in-degree (number of prerequisites) for each course
9 int[] inDegree = new int[numCourses];
10
11 // Build the graph and calculate in-degrees
12 // For prerequisite [a, b]: course a depends on course b
13 // So we add edge from b -> a
14 for (int[] prerequisite : prerequisites) {
15 int course = prerequisite[0];
16 int prerequisiteCourse = prerequisite[1];
17 graph[prerequisiteCourse].add(course);
18 inDegree[course]++;
19 }
20
21 // Queue to store courses with no prerequisites (in-degree = 0)
22 Deque<Integer> queue = new ArrayDeque<>();
23
24 // Add all courses with no prerequisites to the queue
25 for (int course = 0; course < numCourses; course++) {
26 if (inDegree[course] == 0) {
27 queue.offer(course);
28 }
29 }
30
31 // Process courses using topological sort (Kahn's algorithm)
32 while (!queue.isEmpty()) {
33 // Take a course with no remaining prerequisites
34 int currentCourse = queue.poll();
35 // Decrement the count of remaining courses to process
36 numCourses--;
37
38 // For each course that depends on the current course
39 for (int dependentCourse : graph[currentCourse]) {
40 // Reduce its in-degree since we've completed a prerequisite
41 inDegree[dependentCourse]--;
42 // If all prerequisites are met, add to queue
43 if (inDegree[dependentCourse] == 0) {
44 queue.offer(dependentCourse);
45 }
46 }
47 }
48
49 // If all courses are processed (numCourses = 0), no cycle exists
50 return numCourses == 0;
51 }
52}
53
1class Solution {
2public:
3 bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
4 // Build adjacency list graph where edge from 'from' to 'to' means
5 // 'from' is a prerequisite of 'to'
6 vector<vector<int>> adjacencyList(numCourses);
7
8 // Track in-degree (number of prerequisites) for each course
9 vector<int> inDegree(numCourses);
10
11 // Build the graph and calculate in-degrees
12 for (auto& prerequisite : prerequisites) {
13 int course = prerequisite[0];
14 int prerequisiteCourse = prerequisite[1];
15
16 // Add edge from prerequisite to course
17 adjacencyList[prerequisiteCourse].push_back(course);
18
19 // Increment in-degree for the course
20 ++inDegree[course];
21 }
22
23 // Initialize queue with all courses that have no prerequisites (in-degree = 0)
24 queue<int> processQueue;
25 for (int course = 0; course < numCourses; ++course) {
26 if (inDegree[course] == 0) {
27 processQueue.push(course);
28 }
29 }
30
31 // Process courses using topological sort (Kahn's algorithm)
32 while (!processQueue.empty()) {
33 // Take a course with no remaining prerequisites
34 int currentCourse = processQueue.front();
35 processQueue.pop();
36
37 // Mark this course as completed
38 --numCourses;
39
40 // For each course that depends on the current course
41 for (int dependentCourse : adjacencyList[currentCourse]) {
42 // Decrement its in-degree (one prerequisite completed)
43 if (--inDegree[dependentCourse] == 0) {
44 // If all prerequisites are met, add to queue
45 processQueue.push(dependentCourse);
46 }
47 }
48 }
49
50 // If all courses were processed, no cycle exists
51 return numCourses == 0;
52 }
53};
54
1/**
2 * Determines if all courses can be completed given the prerequisites.
3 * Uses topological sorting with Kahn's algorithm to detect cycles in the directed graph.
4 *
5 * @param numCourses - Total number of courses labeled from 0 to numCourses-1
6 * @param prerequisites - Array of prerequisite pairs where [a, b] means course a requires course b
7 * @returns true if all courses can be finished, false if there's a circular dependency
8 */
9function canFinish(numCourses: number, prerequisites: number[][]): boolean {
10 // Build adjacency list graph where graph[i] contains all courses that depend on course i
11 const graph: number[][] = Array.from({ length: numCourses }, () => []);
12
13 // Track the in-degree (number of prerequisites) for each course
14 const inDegree: number[] = Array(numCourses).fill(0);
15
16 // Build the graph and calculate in-degrees
17 for (const [course, prerequisite] of prerequisites) {
18 graph[prerequisite].push(course); // Add edge from prerequisite to course
19 inDegree[course]++; // Increment in-degree for the dependent course
20 }
21
22 // Initialize queue with all courses that have no prerequisites (in-degree = 0)
23 const queue: number[] = [];
24 for (let courseId = 0; courseId < numCourses; courseId++) {
25 if (inDegree[courseId] === 0) {
26 queue.push(courseId);
27 }
28 }
29
30 // Process courses in topological order
31 for (const currentCourse of queue) {
32 numCourses--; // Decrement count of remaining courses to process
33
34 // For each course that depends on the current course
35 for (const dependentCourse of graph[currentCourse]) {
36 // Decrement in-degree and add to queue if all prerequisites are met
37 if (--inDegree[dependentCourse] === 0) {
38 queue.push(dependentCourse);
39 }
40 }
41 }
42
43 // If all courses were processed (numCourses = 0), there's no cycle
44 return numCourses === 0;
45}
46
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm builds an adjacency list representation of the graph and performs topological sorting using Kahn's algorithm. Breaking down the operations:
- Building the graph and calculating in-degrees:
O(m)
- iterates through all prerequisites once - Finding initial nodes with zero in-degree:
O(n)
- checks all courses once - Processing the queue: Each course is added to and removed from the queue at most once (
O(n)
), and each edge is examined exactly once when decrementing in-degrees (O(m)
)
Total time complexity: O(n + m)
Space Complexity: O(n + m)
The space usage consists of:
- Graph adjacency list
g
:O(n + m)
-n
lists storing a total ofm
edges - In-degree array
indeg
:O(n)
- stores in-degree for each course - Queue
q
:O(n)
- in worst case, all courses could be in the queue
Total space complexity: O(n + m)
Where n
represents the number of courses (numCourses
) and m
represents the number of prerequisites (length of prerequisites
list).
Common Pitfalls
1. Misunderstanding the Prerequisite Direction
The Pitfall:
One of the most common mistakes is reversing the prerequisite relationship. When given [a, b]
, many people incorrectly interpret it as "course a is a prerequisite for course b" instead of the correct interpretation: "course b is a prerequisite for course a" (you must take b before a).
Incorrect Implementation:
# WRONG: This reverses the dependency direction for course, prerequisite in prerequisites: graph[course].append(prerequisite) # Wrong direction! in_degree[prerequisite] += 1 # Wrong course!
Why This Causes Problems:
- The graph edges point in the wrong direction
- In-degrees are calculated for the wrong courses
- The topological sort will fail to detect actual cycles
- May return incorrect results for valid course schedules
The Fix:
Always remember that [a, b]
means "b β a" in the dependency graph (b must come before a):
# CORRECT: prerequisite must be taken before course for course, prerequisite in prerequisites: graph[prerequisite].append(course) # prerequisite points to course in_degree[course] += 1 # course has a prerequisite
2. Using a Traditional Queue Instead of Dynamic List Iteration
The Pitfall:
Using a collections.deque and the traditional BFS pattern with while queue:
and popleft()
:
from collections import deque
queue = deque([course for course, degree in enumerate(in_degree) if degree == 0])
while queue:
current = queue.popleft()
numCourses -= 1
for dependent in graph[current]:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
Why The Original Approach Works Better:
While both approaches are correct, the list iteration approach (for i in queue:
) is often cleaner and slightly more efficient in Python because:
- No need to import deque
- Python's list iteration with dynamic appending is optimized
- Simpler code with fewer operations
- The order of processing doesn't matter for cycle detection
3. Modifying the Original numCourses Parameter
The Pitfall:
While the solution correctly uses numCourses -= 1
to count processed courses, this modifies the input parameter, which could be confusing or problematic if the value is needed elsewhere.
Better Practice: Use a separate counter variable:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
# ... graph building code ...
processed_count = 0 # Separate counter
for current_course in queue:
processed_count += 1 # Increment processed courses
for dependent_course in graph[current_course]:
in_degree[dependent_course] -= 1
if in_degree[dependent_course] == 0:
queue.append(dependent_course)
return processed_count == numCourses # Compare with original value
This makes the code more readable and preserves the original parameter value.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!