Facebook Pixel

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:

  1. Building a graph where edges represent prerequisite relationships
  2. Tracking the in-degree (number of prerequisites) for each course
  3. Starting with courses that have no prerequisites (in-degree of 0)
  4. Processing courses level by level, reducing in-degrees of dependent courses
  5. 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 course b to course a.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. First layer: courses with no prerequisites
  2. Second layer: courses whose only prerequisites were in the first layer
  3. Third layer: courses whose prerequisites were all in the first two layers
  4. 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 Evaluator

Example 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 q
    • indeg[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, returning false

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 of m 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More