AlgoMaster Logo

Course Schedule IV

mediumFrequencyUpdated September 21, 2026

Understanding the Problem

We're given a directed acyclic graph (DAG) where each edge [a, b] means "course a must be taken before course b." Each query [u, v] asks whether course u is a prerequisite of course v. Indirect prerequisites count, so if u reaches v through any chain of edges, the answer is true.

The challenge is efficiency. We could answer each query independently by running a graph search, but that repeats work when there are thousands of queries. The alternative is to precompute reachability for the entire graph once, then answer each query in O(1).

This is a transitive closure problem: given a directed graph, determine for every pair (u, v) whether there's a path from u to v.

Key Constraints:

  • numCourses <= 100 → With at most 100 nodes, an O(n^3) algorithm costs about a million operations, so Floyd-Warshall is affordable.
  • prerequisites.length <= n*(n-1)/2 → The graph can be dense, up to ~5,000 edges for n=100, which matters when choosing between edge-driven and matrix-based algorithms.
  • queries.length <= 10^4 → Up to 10,000 queries. If we precompute reachability, each query is O(1). Without precomputation, running BFS/DFS per query gives O(q * (n + e)), which is about 50 million operations. Precomputation is the better strategy.
  • The graph has no cycles, so it's a DAG. This opens up topological sort-based approaches.

Approach 1: BFS/DFS for Each Query

Intuition

For each query [u, v], run a BFS or DFS starting from node u and check whether the search reaches node v. If it does, the answer is true. Otherwise, it's false.

This is correct and simple, but it repeats work. Queries that start from the same node traverse the same portion of the graph again, and nothing learned during one search carries over to the next.

Algorithm

  1. Build an adjacency list from the prerequisites.
  2. For each query [u, v], run a BFS/DFS starting from u.
  3. If the search visits v, add true to the result. Otherwise, add false.
  4. Return the result list.

Visualization and Code

Loading animation...

A partial fix is to cache results per source: run BFS at most once for each distinct starting node and reuse its visited set across queries, for O(n * (n + e)) total search work. Precomputing reachability for every pair goes one step further and makes each query a table lookup.

Approach 2: Floyd-Warshall (Transitive Closure)

Intuition

Instead of answering queries one at a time, build a complete reachability matrix: for every pair of nodes (i, j), record whether i can reach j. Once the matrix exists, every query is a constant-time lookup.

The Floyd-Warshall algorithm computes this. Originally designed for all-pairs shortest paths, the same triple loop computes transitive closure: for each intermediate node k, check whether going through k connects a pair (i, j). If i can reach k and k can reach j, then i can reach j.

With n at most 100, the O(n^3) cost is about one million operations.

Algorithm

  1. Create an n x n boolean matrix reachable, initialized to all false.
  2. For each prerequisite edge [a, b], set reachable[a][b] = true.
  3. Run Floyd-Warshall: for each intermediate node k (0 to n-1), for each pair (i, j), if reachable[i][k] and reachable[k][j] are both true, set reachable[i][j] = true.
  4. For each query [u, v], return reachable[u][v].

Visualization and Code

Loading animation...

The triple loop updates one boolean at a time. Packing each row of the matrix into machine words lets the inner loop process 64 entries per operation.

Approach 3: Bitset Floyd-Warshall

Intuition

Row i of the reachability matrix is a sequence of n booleans, one per course. Stored as a bitset, the entire inner j loop of Floyd-Warshall collapses into one row-wide OR: if i reaches k, then everything k reaches becomes reachable from i, written as reach[i] |= reach[k]. A 64-bit word covers 64 matrix entries at once, so the n^3 single-boolean updates become about n^2 * n/64 word operations.

Merging whole rows is safe because row k cannot change during iteration k. The only update that could touch it is OR-ing row k into itself, which leaves it unchanged. So every row merged into row i is identical to what the element-by-element version would have read.

Algorithm

  1. Create one bit row per course. For each prerequisite edge [a, b], set bit b in row a.
  2. For each intermediate node k from 0 to n-1: for every row i whose bit k is set, OR row k into row i.
  3. For each query [u, v], answer with bit v of row u.

Visualization and Code

Loading animation...

Every Floyd-Warshall variant does work proportional to n^3 (divided by the word size at best) no matter how many edges exist. The final approach uses the DAG structure directly and propagates reachability only along actual edges.

Approach 4: Topological Sort with Reachability Propagation

Intuition

Since the graph is a DAG, we can process nodes in topological order: when a node is processed, every node that points into it has already been processed, so reachability information can be pushed forward along edges.

Each node maintains a set of all nodes that are prerequisites of it (its ancestors). When we process a node u with a neighbor v (meaning u is a prerequisite of v), we add u and all of u's ancestors to v's ancestor set. After every node is processed, a query [u, v] is a membership check on v's ancestor set.

Each edge triggers a set union of up to n elements, so propagation costs O(n * e) in the worst case. On sparse graphs this beats Floyd-Warshall's fixed O(n^3); on dense graphs, where e approaches n^2/2, the two are comparable and the bitset version wins on constants.

Algorithm

  1. Build an adjacency list and compute in-degrees for each node.
  2. Initialize a queue with all nodes that have in-degree 0 (no prerequisites).
  3. For each node, maintain a set of all its ancestors (prerequisites).
  4. Process nodes in topological order (BFS with in-degree tracking):
    • Dequeue a node u.
    • For each neighbor v of u: add u to v's ancestor set, union u's ancestor set into v's ancestor set, and decrement v's in-degree. If v's in-degree reaches 0, enqueue it.
  5. For each query [u, v], check if u is in v's ancestor set.

Visualization and Code

Loading animation...