210. Course Schedule II - Explanation

Problem Link

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.

  • For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.

There are a total of numCourses courses you are required to take, labeled from 0 to numCourses - 1.

Return a valid ordering of courses you can take to finish all courses. If there are many valid answers, return any of them. If it's not possible to finish all courses, return an empty array.

Example 1:

Input: numCourses = 3, prerequisites = [[1,0]]

Output: [0,1,2]

Explanation: We must ensure that course 0 is taken before course 1.

Example 2:

Input: numCourses = 3, prerequisites = [[0,1],[1,2],[2,0]]

Output: []

Explanation: It's impossible to finish all courses.

Constraints:

  • 1 <= numCourses <= 1000
  • 0 <= prerequisites.length <= 1000
  • All prerequisite pairs 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 a cycle in a graph and also find the valid ordering if a cycle doesn't exist?


Hint 2

We can use DFS to detect a cycle in a graph. However, we also need to find the valid ordering of the courses, which can also be achieved using DFS. Alternatively, we can use the Topological Sort algorithm to find the valid ordering in this directed graph, where the graph must be acyclic to complete all the courses, and the prerequisite of a course acts as the parent node of that course. How would you implement this?


Hint 3

We compute the indegrees of all the nodes. Then, we perform a BFS starting from the nodes that have no parents (indegree[node] == 0). At each level, we traverse these nodes, decrement the indegree of their child nodes, and append those child nodes to the queue if their indegree becomes 0. We only append nodes whose indegree is 0 or becomes 0 during the BFS to our result array. If the length of the result array is not equal to the number of courses, we return an empty array.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



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 all paths and detecting cycles
  • Topological Sort - Ordering nodes in a directed acyclic graph (DAG) so dependencies come first
  • Cycle Detection - Identifying cycles in directed graphs using visited/visiting state tracking

1. Cycle Detection (DFS)

Intuition

Each course is a node, and each prerequisite is a directed edge.
We want an order of courses such that all prerequisites of a course are taken before it.

Using DFS, we:

  • Detect cycles (which make it impossible to finish all courses)
  • Add a course to the result after all its prerequisites are processed
    (this naturally gives a valid topological order)

Algorithm

  1. Build a graph where each course points to its prerequisites.
  2. Use two sets:
    • cycle - tracks the current DFS path (for cycle detection)
    • visit - tracks fully processed courses
  3. For each course, run DFS:
    • If the course is already in cycle, a cycle exists - return empty list
    • DFS all prerequisites first
    • After processing prerequisites, add the course to the result
  4. If all DFS calls succeed, return the result list (valid course order)
class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        prereq = {c: [] for c in range(numCourses)}
        for crs, pre in prerequisites:
            prereq[crs].append(pre)

        output = []
        visit, cycle = set(), set()

        def dfs(crs):
            if crs in cycle:
                return False
            if crs in visit:
                return True

            cycle.add(crs)
            for pre in prereq[crs]:
                if dfs(pre) == False:
                    return False
            cycle.remove(crs)
            visit.add(crs)
            output.append(crs)
            return True

        for c in range(numCourses):
            if dfs(c) == False:
                return []
        return output

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of courses and EE 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.
A course can be taken only when all its prerequisites are completed.

Kahn's Algorithm works by:

  • Always taking courses with no remaining prerequisites (indegree = 0)
  • Removing them from the graph
  • Gradually unlocking other courses

If at the end some courses are still locked, it means a cycle exists, so no valid order is possible.

Algorithm

  1. Build a graph and compute indegree for each course
    (indegree = number of prerequisites).
  2. Add all courses with indegree = 0 to a queue.
  3. While the queue is not empty:
    • Remove a course and add it to the result.
    • Reduce the indegree of its dependent courses.
    • If any dependent course reaches indegree = 0, add it to the queue.
  4. If all courses are processed, return the result (reverse if edges were stored as course - prerequisite).
  5. Otherwise, return an empty list (cycle detected).
class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        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, output = 0, []
        while q:
            node = q.popleft()
            output.append(node)
            finish += 1
            for nei in adj[node]:
                indegree[nei] -= 1
                if indegree[nei] == 0:
                    q.append(nei)

        if finish != numCourses:
            return []
        return output[::-1]

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of courses and EE is the number of prerequisites.


3. Topological Sort (DFS)

Intuition

We want an order of courses such that every course appears after its prerequisites.
This approach mixes Topological Sorting with DFS-style traversal.

The idea is:

  • Start from courses that have no prerequisites (indegree = 0)
  • Once we take a course, we "remove" it by decreasing the indegree of courses that depend on it
  • When a dependent course's indegree becomes 0, it is now safe to take, so we continue DFS from it

If we can visit all courses this way, a valid order exists.
If not, a cycle is present, making it impossible.

Algorithm

  1. Build a directed graph from prerequisites and compute indegree for each course.
  2. For every course with indegree = 0, start a DFS.
  3. In DFS:
    • Add the current course to the result.
    • Decrease indegree of its neighbors.
    • If any neighbor's indegree becomes 0, recursively DFS on it.
  4. After traversal:
    • If result contains all courses, return it.
    • Otherwise, return an empty list (cycle detected).
class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        adj = [[] for i in range(numCourses)]
        indegree = [0] * numCourses
        for nxt, pre in prerequisites:
            indegree[nxt] += 1
            adj[pre].append(nxt)

        output = []

        def dfs(node):
            output.append(node)
            indegree[node] -= 1
            for nei in adj[node]:
                indegree[nei] -= 1
                if indegree[nei] == 0:
                    dfs(nei)

        for i in range(numCourses):
            if indegree[i] == 0:
                dfs(i)

        return output if len(output) == numCourses else []

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of courses and EE is the number of prerequisites.


Common Pitfalls

Confusing Cycle Detection with Visited Tracking

In DFS-based topological sort, you need two separate tracking mechanisms: one for nodes currently in the DFS path (for cycle detection) and one for fully processed nodes. Using only one set leads to either false cycle detection or infinite loops.

# Wrong: Using single visited set
def dfs(course):
    if course in visited:
        return False  # Incorrectly treats revisit as cycle
    visited.add(course)
    for pre in prereq[course]:
        if not dfs(pre):
            return False
    return True

# Correct: Separate cycle and visit tracking
def dfs(course):
    if course in cycle:  # Currently in path = cycle
        return False
    if course in visit:  # Already processed = skip
        return True
    cycle.add(course)
    for pre in prereq[course]:
        if not dfs(pre):
            return False
    cycle.remove(course)
    visit.add(course)
    return True

Building the Graph in the Wrong Direction

The prerequisite pair [a, b] means "to take course a, you must first take course b". Building edges in the wrong direction results in an incorrect topological order.

# Wrong: Edge direction reversed
for crs, pre in prerequisites:
    adj[pre].append(crs)  # This builds "pre -> crs" but for Kahn's we may need opposite

# The correct direction depends on your algorithm:
# For DFS that adds after processing: course -> prerequisites
# For Kahn's: prerequisite -> course (then reverse) or course -> prerequisite

Forgetting to Handle Disconnected Courses

Courses with no prerequisites and no dependents still need to be included in the output. Forgetting to iterate over all courses means some valid courses might be missing from the result.

# Wrong: Only processing courses that appear in prerequisites
for crs, pre in prerequisites:
    # Only touches courses mentioned in prerequisites

# Correct: Process all courses from 0 to numCourses-1
for i in range(numCourses):
    if indegree[i] == 0:
        queue.append(i)