207. Course Schedule - 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.

The pair [0, 1], indicates that must take course 1 before taking course 0.

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

Return true if it is possible to finish all courses, otherwise return false.

Example 1:

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

Output: true

Explanation: First take course 1 (no prerequisites) and then take course 0.

Example 2:

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

Output: false

Explanation: In order to take course 1 you must take course 0, and to take course 0 you must take course 1. So it is impossible.

Constraints:

  • 1 <= numCourses <= 1000
  • 0 <= prerequisites.length <= 1000
  • prerequisites[i].length == 2
  • 0 <= a[i], b[i] < numCourses
  • 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 cycle in a graph?


Hint 2

We can use the Depth First Search (DFS) algorithm to detect a cycle in a graph. We iterate over each course, run a DFS from that course, and first try to finish its prerequisite courses by recursively traversing through them. To detect a cycle, we initialize a hash set called path, which contains the nodes visited in the current DFS call. If we encounter a course that is already in the path, we can conclude that a cycle is detected. How would you implement it?


Hint 3

We run a DFS starting from each course by initializing a hash set, path, to track the nodes in the current DFS call. At each step of the DFS, we return false if the current node is already in the path, indicating a cycle. We recursively traverse the neighbors of the current node, and if any of the neighbor DFS calls detect a cycle, we immediately return false. Additionally, we clear the neighbors list of a node when no cycle is found from that node to avoid revisiting those paths again.


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 dependencies and detecting cycles
  • Topological Sort (Kahn's Algorithm) - BFS-based approach using indegree to determine if a valid ordering exists
  • Cycle Detection - Tracking nodes in the current recursion path to identify back edges indicating cycles

1. Cycle Detection (DFS)

Intuition

Each course is a node, and each prerequisite is a directed edge.
You can finish all courses only if there is no cycle in this directed graph.

A cycle means:

  • Course A needs B
  • B needs C
  • C needs A
    So you’re stuck forever.

We use DFS with cycle detection:

  • While doing DFS, keep track of courses in the current recursion path.
  • If we visit a course already in the current path → cycle found.
  • If a course has no prerequisites left, it’s safe.

Algorithm

  1. Build a graph where each course points to its prerequisites.
  2. Use a visiting set to track the current DFS path.
  3. For each course:
    • Run DFS.
    • If the course is already in visiting, return false (cycle).
    • Recursively DFS its prerequisites.
  4. After successfully processing a course, clear its prerequisite list (mark as done).
  5. If all courses are processed without cycles, return true.
class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        # Map each course to its prerequisites
        preMap = {i: [] for i in range(numCourses)}
        for crs, pre in prerequisites:
            preMap[crs].append(pre)

        # Store all courses along the current DFS path
        visiting = set()

        def dfs(crs):
            if crs in visiting:
                # Cycle detected
                return False
            if preMap[crs] == []:
                return True

            visiting.add(crs)
            for pre in preMap[crs]:
                if not dfs(pre):
                    return False
            visiting.remove(crs)
            preMap[crs] = []
            return True

        for c in range(numCourses):
            if not dfs(c):
                return False
        return True

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.
If a course has no prerequisites, it can be taken immediately.

Kahn's Algorithm repeatedly takes courses that have zero prerequisites.
When we finish a course, we remove its dependency effect from other courses.

  • If all courses can be taken this way - no cycle, return true
  • If some courses are never taken - cycle exists, return false

Algorithm

  1. Build a graph and compute indegree (number of prerequisites) for each course.
  2. Add all courses with indegree = 0 into a queue.
  3. While the queue is not empty:
    • Remove a course from the queue.
    • Mark it as finished.
    • Reduce the indegree of its dependent courses.
    • If any dependent course reaches indegree = 0, add it to the queue.
  4. After processing:
    • If finished courses == total courses - return true
    • Else - return false
class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        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 = 0
        while q:
            node = q.popleft()
            finish += 1
            for nei in adj[node]:
                indegree[nei] -= 1
                if indegree[nei] == 0:
                    q.append(nei)

        return finish == numCourses

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

Using a Single Visited Set Instead of Tracking the Current Path

A single visited set marks nodes as seen globally, but cycle detection requires knowing if a node is in the current DFS path. Without tracking the recursion path separately, you may miss cycles or falsely detect them.

# Wrong: single visited set
visited = set()
def dfs(node):
    if node in visited:
        return False  # This doesn't distinguish "finished" from "in current path"
    visited.add(node)
    for nei in adj[node]:
        if not dfs(nei):
            return False
    return True

# Correct: track visiting (current path) and visited (finished)
visiting = set()
def dfs(node):
    if node in visiting:
        return False  # Cycle: node is in current recursion path
    if preMap[node] == []:
        return True   # Already processed, no cycle
    visiting.add(node)
    for nei in preMap[node]:
        if not dfs(nei):
            return False
    visiting.remove(node)
    preMap[node] = []  # Mark as processed
    return True

Forgetting to Handle Disconnected Components

If you only start DFS from one node, you may miss cycles in disconnected parts of the graph. Always iterate through all courses and run DFS from each unvisited node.

# Wrong: only check from course 0
return dfs(0)

# Correct: check all courses
for c in range(numCourses):
    if not dfs(c):
        return False
return True

Incorrectly Building the Adjacency List

The prerequisite [a, b] means "to take course a, you must first take course b". Mixing up which course maps to which leads to checking the wrong dependencies and missing or falsely reporting cycles.