Facebook Pixel

210. Course Schedule II

Problem Description

You need to complete a total of numCourses courses, which are labeled from 0 to numCourses - 1. Some courses have prerequisites that must be completed before you can take them.

The prerequisites are given as an array called prerequisites, where each element prerequisites[i] = [ai, bi] means that you must complete course bi before you can take course ai.

For example, if you have [0, 1], it means you must complete course 1 before taking course 0.

Your task is to find a valid order in which to take all the courses. If multiple valid orderings exist, you can return any one of them. If it's impossible to complete all courses (for instance, due to circular dependencies where course A requires B, and B requires A), return an empty array.

The solution uses topological sorting with Kahn's algorithm:

  1. Build a graph and track dependencies: Create an adjacency list g where g[b] contains all courses that depend on course b. Also maintain an indeg array that counts how many prerequisites each course has.

  2. Find starting courses: Identify all courses with no prerequisites (indegree = 0) and add them to a queue. These can be taken immediately.

  3. Process courses level by level:

    • Remove a course from the queue and add it to the result
    • For each course that depends on the current course, decrease its prerequisite count
    • If any course now has 0 prerequisites remaining, add it to the queue
  4. Check if all courses are taken: If the result contains all numCourses courses, return the order. Otherwise, there's a cycle in the dependencies, making it impossible to complete all courses, so return an empty array.

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 (nodes) and prerequisites (directed edges). 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 find a valid ordering of courses respecting prerequisites. This is only possible if the graph is acyclic (no circular dependencies). If there's a cycle, it's impossible to complete all courses.

Topological Sort

  • Yes: This is the exact algorithm needed. Topological sorting finds a linear ordering of vertices in a directed acyclic graph such that for every directed edge (u, v), vertex u comes before v in the ordering.

Conclusion: The flowchart leads us to use Topological Sort, which can be implemented using either DFS (with cycle detection) or BFS (Kahn's algorithm). The provided solution uses Kahn's algorithm with BFS, tracking indegrees to process nodes with no remaining dependencies first. While the flowchart points to topological sort through the DAG path, DFS is also a valid approach for implementing topological sort with proper cycle detection.

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

Intuition

Think of taking courses like getting dressed - you need to put on your socks before your shoes, and your shirt before your jacket. There's a natural ordering that must be followed.

When we look at the course prerequisites, we're essentially dealing with dependencies. Course A depends on Course B means B must come before A. This creates a directed graph where an edge from B to A represents "B must be completed before A."

The key insight is recognizing that if we can complete all courses, the dependency graph must not have any cycles. Why? Because a cycle would mean Course A requires B, B requires C, and C requires A - creating an impossible situation where you can't start any of these courses.

Once we know the graph must be acyclic, the problem becomes: "How do we find an ordering that respects all dependencies?" This is exactly what topological sorting does.

The approach using Kahn's algorithm is intuitive:

  1. Start with courses that have no prerequisites - these can be taken immediately
  2. Once we complete a course, we "unlock" other courses that depend on it
  3. Keep track of how many prerequisites each course still needs (indegree)
  4. When a course's prerequisite count drops to 0, it becomes available to take

It's like peeling an onion layer by layer - we start from the outermost layer (courses with no prerequisites) and work our way inward, removing dependencies as we go. If we can peel away all layers and process all courses, we have a valid ordering. If some courses remain stuck because of circular dependencies, it's impossible to complete them all.

The beauty of this approach is that it naturally detects cycles - if there's a cycle, some courses will never have their prerequisite count drop to 0, so we won't be able to include all courses in our final ordering.

Solution Approach

The implementation uses Kahn's Algorithm for topological sorting with BFS. Let's walk through the key components:

1. Build the Graph and Track Dependencies

g = defaultdict(list)
indeg = [0] * numCourses
for a, b in prerequisites:
    g[b].append(a)
    indeg[a] += 1
  • g is an adjacency list where g[b] contains all courses that depend on course b
  • indeg[a] counts how many prerequisites course a has (in-degree)
  • For each prerequisite [a, b], we add an edge from b to a and increment a's in-degree

2. Initialize Queue with Starting Courses

ans = []
q = deque(i for i, x in enumerate(indeg) if x == 0)
  • Find all courses with 0 prerequisites (in-degree = 0)
  • These courses can be taken immediately, so add them to the queue
  • ans will store our final course ordering

3. Process Courses Level by Level

while q:
    i = q.popleft()
    ans.append(i)
    for j in g[i]:
        indeg[j] -= 1
        if indeg[j] == 0:
            q.append(j)
  • Remove a course i from the queue and add it to our result
  • For each course j that depends on i:
    • Decrease its prerequisite count by 1 (we've completed one of its prerequisites)
    • If j now has 0 prerequisites remaining, add it to the queue

4. Verify All Courses Are Completed

return ans if len(ans) == numCourses else []
  • If we've added all numCourses to our result, we found a valid ordering
  • If not, there must be a cycle preventing some courses from being taken, so return empty array

Time Complexity: O(V + E) where V is the number of courses and E is the number of prerequisites. We visit each course once and process each prerequisite edge once.

Space Complexity: O(V + E) for storing the graph, in-degree array, and queue.

The algorithm elegantly handles cycle detection - if there's a cycle, the courses involved will never have their in-degree drop to 0, so they won't be added to the result, causing the final check to fail.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with 4 courses and 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 Track Dependencies

After processing prerequisites:

  • g = {0: [1, 2], 1: [3], 2: [3]}
  • indeg = [0, 1, 1, 2]

The graph looks like:

    0
   / \
  1   2
   \ /
    3

Step 2: Initialize Queue with Starting Courses

Course 0 has indegree 0 (no prerequisites), so:

  • q = [0]
  • ans = []

Step 3: Process Courses Level by Level

Iteration 1:

  • Pop course 0 from queue
  • Add 0 to result: ans = [0]
  • Process courses depending on 0:
    • Course 1: indeg[1] = 1 - 1 = 0 β†’ Add to queue
    • Course 2: indeg[2] = 1 - 1 = 0 β†’ Add to queue
  • Queue state: q = [1, 2]

Iteration 2:

  • Pop course 1 from queue
  • Add 1 to result: ans = [0, 1]
  • Process courses depending on 1:
    • Course 3: indeg[3] = 2 - 1 = 1 β†’ Still has prerequisites, don't add
  • Queue state: q = [2]

Iteration 3:

  • Pop course 2 from queue
  • Add 2 to result: ans = [0, 1, 2]
  • Process courses depending on 2:
    • Course 3: indeg[3] = 1 - 1 = 0 β†’ Add to queue
  • Queue state: q = [3]

Iteration 4:

  • Pop course 3 from queue
  • Add 3 to result: ans = [0, 1, 2, 3]
  • No courses depend on 3
  • Queue state: q = [] (empty)

Step 4: Verify All Courses Are Completed

len(ans) = 4 = numCourses, so return [0, 1, 2, 3]

The algorithm correctly found that we must take course 0 first, then courses 1 and 2 (in either order), and finally course 3 after both its prerequisites are satisfied.

Solution Implementation

1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
6        # Build adjacency list graph where key is prerequisite, value is list of courses that depend on it
7        graph = defaultdict(list)
8        # Track the number of prerequisites (in-degree) for each course
9        in_degree = [0] * numCourses
10      
11        # Build the graph and calculate in-degrees
12        for course, prerequisite in prerequisites:
13            graph[prerequisite].append(course)
14            in_degree[course] += 1
15      
16        # Initialize result list to store the course order
17        result = []
18      
19        # Initialize queue with all courses that have no prerequisites (in-degree = 0)
20        queue = deque([course for course, degree in enumerate(in_degree) if degree == 0])
21      
22        # Process courses using Kahn's algorithm (topological sort)
23        while queue:
24            # Take a course with no remaining prerequisites
25            current_course = queue.popleft()
26            result.append(current_course)
27          
28            # For each course that depends on the current course
29            for dependent_course in graph[current_course]:
30                # Decrease the in-degree since we've completed a prerequisite
31                in_degree[dependent_course] -= 1
32                # If all prerequisites are completed, add to queue
33                if in_degree[dependent_course] == 0:
34                    queue.append(dependent_course)
35      
36        # Return the result if all courses are included (no cycle), otherwise return empty list
37        return result if len(result) == numCourses else []
38
1class Solution {
2    public int[] findOrder(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 to 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        // Initialize queue with all courses that have no prerequisites (in-degree = 0)
22        Deque<Integer> queue = new ArrayDeque<>();
23        for (int course = 0; course < numCourses; course++) {
24            if (inDegree[course] == 0) {
25                queue.offer(course);
26            }
27        }
28      
29        // Store the topological order of courses
30        int[] courseOrder = new int[numCourses];
31        int processedCourses = 0;
32      
33        // Process courses using Kahn's algorithm for topological sorting
34        while (!queue.isEmpty()) {
35            // Take a course with no remaining prerequisites
36            int currentCourse = queue.poll();
37            courseOrder[processedCourses++] = currentCourse;
38          
39            // For each course that depends on the current course
40            for (int dependentCourse : graph[currentCourse]) {
41                // Decrease the in-degree since we've completed one prerequisite
42                inDegree[dependentCourse]--;
43                // If all prerequisites are completed, add to queue
44                if (inDegree[dependentCourse] == 0) {
45                    queue.offer(dependentCourse);
46                }
47            }
48        }
49      
50        // If all courses are processed, return the order; otherwise, cycle exists
51        return processedCourses == numCourses ? courseOrder : new int[0];
52    }
53}
54
1class Solution {
2public:
3    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
4        // Build adjacency list graph where edge from 'from' to 'to' means 
5        // course 'from' is a prerequisite for course 'to'
6        vector<vector<int>> adjacencyList(numCourses);
7      
8        // Track the number of prerequisites (incoming edges) for each course
9        vector<int> inDegree(numCourses);
10      
11        // Process prerequisites to build 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 that has this prerequisite
20            ++inDegree[course];
21        }
22      
23        // Queue for BFS traversal - start with courses that have no prerequisites
24        queue<int> courseQueue;
25      
26        // Find all courses with no prerequisites (in-degree = 0)
27        for (int courseId = 0; courseId < numCourses; ++courseId) {
28            if (inDegree[courseId] == 0) {
29                courseQueue.push(courseId);
30            }
31        }
32      
33        // Result vector to store the course order
34        vector<int> courseOrder;
35      
36        // Process courses using Kahn's algorithm (topological sort)
37        while (!courseQueue.empty()) {
38            // Take a course with no remaining prerequisites
39            int currentCourse = courseQueue.front();
40            courseQueue.pop();
41          
42            // Add to result as this course can be taken
43            courseOrder.push_back(currentCourse);
44          
45            // Process all courses that depend on the current course
46            for (int dependentCourse : adjacencyList[currentCourse]) {
47                // Decrease in-degree as one prerequisite is completed
48                if (--inDegree[dependentCourse] == 0) {
49                    // If no more prerequisites remain, add to queue
50                    courseQueue.push(dependentCourse);
51                }
52            }
53        }
54      
55        // If all courses are processed, return the order; otherwise, cycle exists
56        return courseOrder.size() == numCourses ? courseOrder : vector<int>();
57    }
58};
59
1/**
2 * Finds a valid course order given prerequisites using topological sort (Kahn's algorithm)
3 * @param numCourses - Total number of courses to take (labeled from 0 to numCourses - 1)
4 * @param prerequisites - Array where prerequisites[i] = [courseA, courseB] means courseB must be taken before courseA
5 * @returns Array representing a valid course order, or empty array if impossible
6 */
7function findOrder(numCourses: number, prerequisites: number[][]): number[] {
8    // Build adjacency list graph where graph[i] contains courses that depend on course i
9    const graph: number[][] = Array.from({ length: numCourses }, () => []);
10  
11    // Track in-degree (number of prerequisites) for each course
12    const inDegree: number[] = new Array(numCourses).fill(0);
13  
14    // Build the graph and calculate in-degrees
15    for (const [course, prerequisite] of prerequisites) {
16        graph[prerequisite].push(course);
17        inDegree[course]++;
18    }
19  
20    // Initialize queue with all courses that have no prerequisites (in-degree = 0)
21    const queue: number[] = [];
22    for (let courseId = 0; courseId < numCourses; courseId++) {
23        if (inDegree[courseId] === 0) {
24            queue.push(courseId);
25        }
26    }
27  
28    // Process courses in topological order
29    const courseOrder: number[] = [];
30    while (queue.length > 0) {
31        // Take a course with no remaining prerequisites
32        const currentCourse = queue.shift()!;
33        courseOrder.push(currentCourse);
34      
35        // Update dependent courses and add to queue if all prerequisites are met
36        for (const dependentCourse of graph[currentCourse]) {
37            inDegree[dependentCourse]--;
38            if (inDegree[dependentCourse] === 0) {
39                queue.push(dependentCourse);
40            }
41        }
42    }
43  
44    // Return the course order if all courses can be taken, otherwise return empty array
45    return courseOrder.length === numCourses ? courseOrder : [];
46}
47

Time and Space Complexity

Time Complexity: O(V + E) where V is the number of courses (numCourses) and E is the number of prerequisites.

  • Building the adjacency list and indegree array: We iterate through all prerequisites once, which takes O(E) time.
  • Finding initial nodes with indegree 0: We iterate through all courses once, which takes O(V) time.
  • BFS traversal: Each course is added to and removed from the queue exactly once, taking O(V) time. For each course, we iterate through its adjacent courses (outgoing edges), and in total, we process all edges exactly once, taking O(E) time.
  • Overall: O(V + E)

Space Complexity: O(V + E)

  • Adjacency list g: In the worst case, stores all edges, requiring O(E) space.
  • Indegree array: Stores the indegree for each course, requiring O(V) space.
  • Queue q: In the worst case, can contain all courses with indegree 0 at once, requiring O(V) space.
  • Answer array ans: Stores the topological order of all courses, requiring O(V) space.
  • Overall: O(V + E)

Common Pitfalls

1. Incorrectly Building the Directed Graph

One of the most common mistakes is reversing the edge direction when building the graph. Given prerequisites[i] = [ai, bi] where course bi must be completed before course ai, many people mistakenly create an edge from ai to bi instead of from bi to ai.

Incorrect Implementation:

# WRONG: This reverses the dependency relationship
for a, b in prerequisites:
    graph[a].append(b)  # Wrong direction!
    in_degree[b] += 1   # Wrong course's in-degree!

Correct Implementation:

# CORRECT: Edge goes from prerequisite to dependent course
for a, b in prerequisites:
    graph[b].append(a)  # b must be taken before a
    in_degree[a] += 1   # a has one more prerequisite

2. Using Wrong Data Structure for the Queue

Using a regular list with pop(0) instead of deque with popleft() leads to O(n) time complexity for each removal operation, significantly impacting performance for large inputs.

Inefficient Implementation:

# INEFFICIENT: O(n) for each pop operation
queue = [course for course, degree in enumerate(in_degree) if degree == 0]
while queue:
    current_course = queue.pop(0)  # O(n) operation!

Efficient Implementation:

# EFFICIENT: O(1) for each popleft operation
from collections import deque
queue = deque([course for course, degree in enumerate(in_degree) if degree == 0])
while queue:
    current_course = queue.popleft()  # O(1) operation

3. Forgetting to Handle the Case with No Prerequisites

If there are no prerequisites at all, some implementations might fail to properly initialize the queue or handle the edge case where all courses can be taken in any order.

Potential Issue:

# If prerequisites is empty, ensure all courses are still added to queue
if not prerequisites:
    return list(range(numCourses))  # Could be handled explicitly

The provided solution handles this correctly by checking all courses' in-degrees, which will all be 0 if there are no prerequisites.

4. Not Verifying All Courses Are Included in the Result

Forgetting the final check can lead to returning an incomplete course list when cycles exist, violating the problem requirements.

Incomplete Solution:

# WRONG: Returns partial result even with cycles
return result  # Missing validation!

Complete Solution:

# CORRECT: Validates that all courses are schedulable
return result if len(result) == numCourses else []

5. Modifying the Original Prerequisites List

Some implementations might accidentally modify the input data, which could cause issues if the function needs to be called multiple times or if the caller expects the input to remain unchanged.

Problematic Approach:

# BAD: Modifies input directly
for prereq in prerequisites:
    prereq[0], prereq[1] = prereq[1], prereq[0]  # Modifies original!

Better Approach:

# GOOD: Works with values without modifying input
for a, b in prerequisites:  # Unpacking doesn't modify original
    graph[b].append(a)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More