title: Course Schedule IV
slug: course-schedule-iv
difficulty: medium
leetcode_id: 1462
leetcode_url: https://leetcode.com/problems/course-schedule-iv/
categories:
  - graphs
patterns:
  - bfs
  - dfs
  - dynamic-programming

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

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

  Prerequisites can also be **indirect**. If course `a` is a prerequisite of course `b`, and course `b` is a prerequisite of course `c`, then course `a` is a prerequisite of course `c`.

  You are also given an array `queries` where `queries[j] = [u_j, v_j]`. For the j<sup>th</sup> query, you should answer whether course `u_j` is a prerequisite of course `v_j` or not.

  Return *a boolean array* `answer`, *where* `answer[j]` *is the answer to the* j<sup>th</sup> *query*.

constraints: |
  - `2 <= numCourses <= 100`
  - `0 <= prerequisites.length <= (numCourses * (numCourses - 1) / 2)`
  - `prerequisites[i].length == 2`
  - `0 <= a_i, b_i <= numCourses - 1`
  - `a_i != b_i`
  - All the pairs `[a_i, b_i]` are **unique**
  - The prerequisites graph has no cycles
  - `1 <= queries.length <= 10^4`
  - `0 <= u_i, v_i <= numCourses - 1`
  - `u_i != v_i`

examples:
  - input: "numCourses = 2, prerequisites = [[1,0]], queries = [[0,1],[1,0]]"
    output: "[false, true]"
    explanation: "The pair [1, 0] indicates that you have to take course 1 before you can take course 0. Course 0 is not a prerequisite of course 1, but the opposite is true."
  - input: "numCourses = 2, prerequisites = [], queries = [[1,0],[0,1]]"
    output: "[false, false]"
    explanation: "There are no prerequisites, and each course is independent."
  - input: "numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]]"
    output: "[true, true]"
    explanation: "Course 1 is a direct prerequisite of both course 2 and course 0. Course 2 is also a prerequisite of course 0, but for these queries we only need to check if course 1 is a prerequisite."

explanation:
  intuition: |
    Think of the courses as cities and prerequisites as one-way roads. The question "Is course `u` a prerequisite of course `v`?" is really asking: **Can you travel from city `u` to city `v` following the roads?**

    This is the classic **graph reachability** problem. Given a directed acyclic graph (DAG), we need to precompute which nodes can reach which other nodes, then answer multiple queries efficiently.

    The key insight is that with `numCourses <= 100` and potentially `10^4` queries, we can afford to precompute **all** reachability information upfront. If we had to run a BFS/DFS for each query, we'd do O(queries * (V + E)) work. Instead, we can precompute a reachability matrix in O(V^3) or O(V * (V + E)) time, then answer each query in O(1).

    The **Floyd-Warshall** algorithm is perfect here: it computes transitive closure (all-pairs reachability) by considering whether any intermediate node `k` can connect node `i` to node `j`. If `i` can reach `k` and `k` can reach `j`, then `i` can reach `j`.

  approach: |
    We solve this using **Floyd-Warshall Transitive Closure**:

    **Step 1: Initialise the reachability matrix**

    - Create an `n x n` boolean matrix `reachable` where `reachable[i][j]` means "course `i` is a prerequisite of course `j`"
    - Initialise all values to `False`
    - For each direct prerequisite `[a, b]`, set `reachable[a][b] = True`

    &nbsp;

    **Step 2: Apply Floyd-Warshall algorithm**

    - For each intermediate course `k` from `0` to `n-1`:
      - For each pair of courses `(i, j)`:
        - If `i` can reach `k` AND `k` can reach `j`, then `i` can reach `j`
        - Update: `reachable[i][j] = reachable[i][j] OR (reachable[i][k] AND reachable[k][j])`

    &nbsp;

    **Step 3: Answer queries**

    - For each query `[u, v]`, simply look up `reachable[u][v]`
    - Return the list of boolean answers

    &nbsp;

    The Floyd-Warshall approach works because it systematically considers all possible "stepping stones" between any two nodes. After processing all intermediate nodes, the matrix contains complete transitive closure information.

  common_pitfalls:
    - title: Running BFS/DFS Per Query
      description: |
        A naive approach runs a graph traversal for each query to check if `u` can reach `v`. With up to `10^4` queries and O(V + E) per traversal, this becomes O(queries * (V + E)).

        While this might pass given the small graph size (V <= 100), it's unnecessarily slow. Precomputing reachability once in O(V^3) or O(V * (V + E)) is more efficient when there are many queries.
      wrong_approach: "BFS/DFS for each query"
      correct_approach: "Precompute all reachability with Floyd-Warshall"

    - title: Confusing Edge Direction
      description: |
        The problem states `prerequisites[i] = [a, b]` means "you must take course `a` first to take course `b`". This means there's a directed edge **from `a` to `b`**.

        A query `[u, v]` asks if `u` is a prerequisite of `v`, meaning "can we reach `v` starting from `u`?" Make sure your edge direction matches: `reachable[a][b] = True` for prerequisite `[a, b]`.
      wrong_approach: "Edge from b to a (reversed)"
      correct_approach: "Edge from a to b (a is prerequisite of b)"

    - title: Forgetting Transitive Relationships
      description: |
        Don't only consider direct prerequisites. If `A -> B` and `B -> C`, then `A` is also a prerequisite of `C` even though `[A, C]` isn't in the input.

        The whole point of Floyd-Warshall (or DFS from each node) is to propagate these indirect relationships. Simply checking if `[u, v]` exists in the prerequisites list will give wrong answers.
      wrong_approach: "Only check direct prerequisites"
      correct_approach: "Compute transitive closure"

  key_takeaways:
    - "**Transitive closure**: Floyd-Warshall computes all-pairs reachability in O(V^3), ideal when V is small but queries are many"
    - "**Precomputation tradeoff**: When you have many queries on static data, precompute everything upfront for O(1) query time"
    - "**Alternative approach**: BFS/DFS from each node also works in O(V * (V + E)) and may be faster for sparse graphs"
    - "**Graph representation**: This pattern applies to any reachability problem - dependency resolution, inheritance hierarchies, social network connections"

  time_complexity: "O(V^3 + Q). Floyd-Warshall takes O(V^3) where V is `numCourses`, and answering Q queries takes O(Q). With V <= 100 and Q <= 10^4, this is efficient."
  space_complexity: "O(V^2). We store an `n x n` reachability matrix where n is `numCourses`."

solutions:
  - approach_name: Floyd-Warshall Transitive Closure
    is_optimal: true
    code: |
      def checkIfPrerequisite(
          numCourses: int,
          prerequisites: list[list[int]],
          queries: list[list[int]]
      ) -> list[bool]:
          n = numCourses
          # Initialise reachability matrix
          # reachable[i][j] = True means course i is a prerequisite of course j
          reachable = [[False] * n for _ in range(n)]

          # Mark direct prerequisites
          for a, b in prerequisites:
              reachable[a][b] = True

          # Floyd-Warshall: propagate reachability through intermediate nodes
          for k in range(n):
              for i in range(n):
                  for j in range(n):
                      # If i reaches k and k reaches j, then i reaches j
                      if reachable[i][k] and reachable[k][j]:
                          reachable[i][j] = True

          # Answer each query in O(1)
          return [reachable[u][v] for u, v in queries]
    explanation: |
      **Time Complexity:** O(V^3 + Q) — Floyd-Warshall runs in O(V^3), then each of Q queries is O(1).

      **Space Complexity:** O(V^2) — The reachability matrix stores n^2 boolean values.

      Floyd-Warshall systematically considers each node `k` as a potential intermediate step. If we can reach `k` from `i` and reach `j` from `k`, we know `i` can reach `j`. After checking all intermediate nodes, we have complete transitive closure.

  - approach_name: BFS from Each Node
    is_optimal: false
    code: |
      from collections import deque

      def checkIfPrerequisite(
          numCourses: int,
          prerequisites: list[list[int]],
          queries: list[list[int]]
      ) -> list[bool]:
          n = numCourses
          # Build adjacency list
          graph = [[] for _ in range(n)]
          for a, b in prerequisites:
              graph[a].append(b)

          # Precompute reachability using BFS from each node
          reachable = [[False] * n for _ in range(n)]

          for start in range(n):
              # BFS to find all nodes reachable from start
              visited = [False] * n
              queue = deque([start])
              visited[start] = True

              while queue:
                  node = queue.popleft()
                  for neighbor in graph[node]:
                      if not visited[neighbor]:
                          visited[neighbor] = True
                          reachable[start][neighbor] = True
                          queue.append(neighbor)

          return [reachable[u][v] for u, v in queries]
    explanation: |
      **Time Complexity:** O(V * (V + E) + Q) — BFS from each of V nodes, each BFS is O(V + E), then Q queries in O(1).

      **Space Complexity:** O(V^2 + V + E) — Reachability matrix O(V^2), adjacency list O(V + E), BFS queue O(V).

      This approach builds an adjacency list and runs BFS from each node to find all reachable nodes. For sparse graphs where E << V^2, this can be faster than Floyd-Warshall. Both approaches precompute full reachability for O(1) query answering.
