title: Course Schedule II
slug: course-schedule-ii
difficulty: medium
leetcode_id: 210
leetcode_url: https://leetcode.com/problems/course-schedule-ii/
categories:
  - graphs
patterns:
  - bfs
  - dfs

description: |
  There are a total of `numCourses` courses you have to take, labelled from `0` to `numCourses - 1`. You are given an array `prerequisites` where `prerequisites[i] = [a_i, b_i]` indicates that you **must** take course `b_i` first if you want to take course `a_i`.

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

  Return *the ordering of courses you should take to finish all courses*. If there are many valid answers, return **any** of them. If it is impossible to finish all courses, return **an empty array**.

constraints: |
  - `1 <= numCourses <= 2000`
  - `0 <= prerequisites.length <= numCourses * (numCourses - 1)`
  - `prerequisites[i].length == 2`
  - `0 <= a_i, b_i < numCourses`
  - `a_i != b_i`
  - All the pairs `[a_i, b_i]` are **distinct**

examples:
  - input: "numCourses = 2, prerequisites = [[1,0]]"
    output: "[0,1]"
    explanation: "There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1]."
  - input: "numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]"
    output: "[0,2,1,3]"
    explanation: "There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3]."
  - input: "numCourses = 1, prerequisites = []"
    output: "[0]"
    explanation: "There is only one course with no prerequisites, so the order is simply [0]."

explanation:
  intuition: |
    Imagine you're planning which university courses to take each semester. Some courses have prerequisites — you can't take Advanced Algorithms until you've completed Data Structures. This creates a natural ordering constraint.

    The problem is asking us to find a **topological ordering** of the courses. Think of it like stacking books: you can only place a book on top of another if all its "dependencies" are already in the stack below. If there's a circular dependency (Book A requires Book B, and Book B requires Book A), it's impossible to stack them — we'd return an empty array.

    The key insight is recognising this as a **directed graph problem**. Each course is a node, and each prerequisite `[a, b]` creates an edge from `b` to `a` (meaning "b must come before a"). We need to find an ordering where every edge points "forward" — that's exactly what topological sort does.

    There are two classic approaches: **Kahn's Algorithm (BFS)** processes courses with no remaining prerequisites first, while **DFS-based** approach explores deeply and adds courses to the result in reverse post-order.

  approach: |
    We'll use **Kahn's Algorithm (BFS)** — it's intuitive and naturally handles cycle detection:

    **Step 1: Build the graph and calculate in-degrees**

    - Create an adjacency list to represent the graph: for each prerequisite `[a, b]`, add `a` to the list of courses that depend on `b`
    - Calculate the **in-degree** of each course: how many prerequisites it has
    - In-degree tells us how many courses must be completed before we can take this one

    &nbsp;

    **Step 2: Initialise the queue with "ready" courses**

    - Add all courses with in-degree `0` to a queue — these have no prerequisites and can be taken immediately
    - These are our starting points

    &nbsp;

    **Step 3: Process courses level by level (BFS)**

    - Dequeue a course, add it to the result
    - For each course that depends on the dequeued course, decrement its in-degree
    - If any course's in-degree becomes `0`, it's now "unlocked" — add it to the queue
    - Repeat until the queue is empty

    &nbsp;

    **Step 4: Check for cycles**

    - If the result contains all `numCourses` courses, return it
    - If not, a cycle exists (some courses could never reach in-degree `0`) — return an empty array

    &nbsp;

    This approach works because we only process a course when all its prerequisites have been processed, naturally building a valid ordering.

  common_pitfalls:
    - title: Cycle Detection Failure
      description: |
        The most critical edge case is detecting **cycles** in the prerequisites. If course A requires B, B requires C, and C requires A, no valid ordering exists.

        With Kahn's Algorithm, cycles are detected automatically: courses in a cycle never reach in-degree `0`, so they're never added to the queue. If our result has fewer than `numCourses` entries, we know there's a cycle.

        Forgetting this check would return an incomplete ordering instead of an empty array.
      wrong_approach: "Return partial ordering without checking length"
      correct_approach: "Verify result length equals numCourses"

    - title: Confusing Edge Direction
      description: |
        The prerequisite `[a, b]` means "b must come before a" — the edge goes FROM `b` TO `a`. Getting this backwards inverts the entire dependency graph.

        Think of it as: "to take course `a`, you need course `b` first." So `b` points to `a` (b enables a).
      wrong_approach: "Add edge from a to b"
      correct_approach: "Add edge from b to a (prerequisite points to dependent)"

    - title: Handling Disconnected Components
      description: |
        Some courses might have no prerequisites AND no courses depending on them (isolated nodes). These must still appear in the result.

        Initialising in-degrees for all courses from `0` to `numCourses - 1` (not just those in prerequisites) ensures isolated courses start with in-degree `0` and get processed.
      wrong_approach: "Only track courses mentioned in prerequisites"
      correct_approach: "Initialise in-degree array for all numCourses courses"

    - title: Multiple Valid Orderings
      description: |
        The problem states any valid ordering is acceptable. Don't over-engineer to find a "canonical" order — the first valid topological sort you find works.

        If courses 1 and 2 both have in-degree 0 at the same time, either can come first in the result.

  key_takeaways:
    - "**Topological sort** produces a linear ordering of vertices in a DAG where every edge points from earlier to later in the sequence"
    - "**Kahn's Algorithm** uses BFS with in-degrees: start with in-degree 0 nodes, decrement neighbours' in-degrees, repeat"
    - "**Cycle detection** is built-in: if the result has fewer nodes than the graph, a cycle exists"
    - "This pattern applies to **dependency resolution** problems: build systems, task scheduling, package managers"

  time_complexity: "O(V + E). We process each course (vertex) once and examine each prerequisite (edge) once when decrementing in-degrees. Here V = `numCourses` and E = `len(prerequisites)`."
  space_complexity: "O(V + E). The adjacency list stores all edges, and we use arrays of size V for in-degrees and the result. The queue holds at most V courses."

solutions:
  - approach_name: Kahn's Algorithm (BFS)
    is_optimal: true
    code: |
      from collections import deque

      def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]:
          # Build adjacency list and calculate in-degrees
          graph = [[] for _ in range(num_courses)]
          in_degree = [0] * num_courses

          for course, prereq in prerequisites:
              graph[prereq].append(course)  # prereq -> course edge
              in_degree[course] += 1  # course has one more prerequisite

          # Start with courses that have no prerequisites
          queue = deque()
          for course in range(num_courses):
              if in_degree[course] == 0:
                  queue.append(course)

          result = []

          # Process courses in topological order
          while queue:
              current = queue.popleft()
              result.append(current)

              # "Unlock" courses that depended on current
              for next_course in graph[current]:
                  in_degree[next_course] -= 1
                  if in_degree[next_course] == 0:
                      queue.append(next_course)

          # If we processed all courses, return the order; otherwise cycle exists
          return result if len(result) == num_courses else []
    explanation: |
      **Time Complexity:** O(V + E) — Each course is enqueued/dequeued once, and each prerequisite edge is processed once.

      **Space Complexity:** O(V + E) — Adjacency list stores all edges, plus O(V) for in-degrees, queue, and result.

      Kahn's Algorithm processes courses in dependency order. By tracking in-degrees, we know exactly when a course becomes "ready" (all prerequisites completed). The BFS naturally produces a valid topological ordering.

  - approach_name: DFS with Cycle Detection
    is_optimal: true
    code: |
      def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]:
          # Build adjacency list
          graph = [[] for _ in range(num_courses)]
          for course, prereq in prerequisites:
              graph[prereq].append(course)

          # States: 0 = unvisited, 1 = visiting (in current path), 2 = visited
          state = [0] * num_courses
          result = []

          def dfs(course: int) -> bool:
              """Returns True if no cycle detected, False if cycle found."""
              if state[course] == 1:  # Currently visiting -> cycle!
                  return False
              if state[course] == 2:  # Already processed
                  return True

              state[course] = 1  # Mark as visiting

              for next_course in graph[course]:
                  if not dfs(next_course):
                      return False

              state[course] = 2  # Mark as fully processed
              result.append(course)  # Add in reverse post-order
              return True

          # Run DFS from each unvisited course
          for course in range(num_courses):
              if state[course] == 0:
                  if not dfs(course):
                      return []

          # Result is in reverse order (we added nodes after processing children)
          return result[::-1]
    explanation: |
      **Time Complexity:** O(V + E) — Each course is visited once, and each edge is traversed once.

      **Space Complexity:** O(V + E) — Adjacency list plus O(V) recursion stack in worst case.

      DFS explores each course deeply, adding it to the result only after all its dependents are processed. The three-state system (unvisited, visiting, visited) detects cycles: if we revisit a node that's currently in our path, we've found a cycle. The final result is reversed because we add nodes in post-order (after children).
