LeetCode 1462 - Course Schedule IV
This problem gives us a directed acyclic graph representing course dependencies. Each course is a node, and a prerequisi
Difficulty: 🟡 Medium
Topics: Depth-First Search, Breadth-First Search, Graph Theory, Topological Sort
Solution
Problem Understanding
This problem gives us a directed acyclic graph representing course dependencies. Each course is a node, and a prerequisite relationship [a, b] means there is a directed edge from a to b. In other words, course a must be completed before course b.
The important detail is that prerequisites are transitive. If course 0 is required before course 1, and course 1 is required before course 2, then course 0 is also considered a prerequisite of course 2. The problem asks us to answer many such reachability questions efficiently.
For each query [u, v], we must determine whether there exists a path from u to v in the graph. If such a path exists, then u is a prerequisite of v.
The input consists of:
numCourses, the total number of nodes in the graphprerequisites, the directed edgesqueries, the reachability checks we need to answer
The output is a boolean array where each position corresponds to one query.
The constraints are very important for selecting the right algorithm:
numCourses <= 100queries <= 10^4
The graph itself is small, but the number of queries can be large. This strongly suggests preprocessing the graph once, then answering each query in constant time.
The graph is guaranteed to have no cycles, meaning it is a Directed Acyclic Graph, or DAG. This guarantee simplifies traversal logic because we never need to worry about infinite loops caused by cycles.
Several edge cases are important:
- There may be no prerequisite relationships at all.
- Queries may ask about unrelated nodes.
- Dependencies may be indirect across many intermediate courses.
- Multiple prerequisite chains may converge into the same course.
- The graph can be disconnected.
A naive solution that performs a fresh traversal for every query may still work for these constraints, but there is a more elegant preprocessing approach that answers every query efficiently.
Approaches
Brute Force Approach
The brute-force idea is straightforward. For every query [u, v], perform a graph traversal starting from u and check whether v is reachable.
We can use either Depth-First Search or Breadth-First Search for each query:
- Build an adjacency list from the prerequisite pairs.
- For every query:
- Start a DFS or BFS from
u - Search until either
vis found or traversal ends
- Return whether
vwas reachable
This works because graph traversal correctly determines reachability in a directed graph.
However, this becomes inefficient when there are many queries. If we perform a traversal for every query, we repeatedly explore the same graph structure many times.
With:
- up to
10^4queries - up to
100nodes
the repeated traversals create unnecessary overhead.
Optimal Approach
The key insight is that the graph is small, but the number of queries is large.
Instead of answering queries independently, we can preprocess all prerequisite relationships in advance.
We create a matrix:
reachable[a][b]
which stores whether course a is a prerequisite of course b.
Since the graph has only 100 nodes, we can compute transitive reachability for all node pairs efficiently.
One elegant way to do this is the Floyd-Warshall transitive closure technique.
The idea is:
-
Initialize direct prerequisite relationships
-
Gradually allow intermediate nodes
-
If:
-
a -> k -
and
k -> b -
then:
-
a -> b
After preprocessing, every query becomes a constant-time lookup.
This is ideal because:
- preprocessing cost is small for
n <= 100 - query answering becomes extremely fast
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(q × (V + E)) | O(V + E) | Runs DFS/BFS for every query |
| Optimal | O(V³ + q) | O(V²) | Precomputes all prerequisite relationships |
Algorithm Walkthrough
We use Floyd-Warshall style transitive closure computation.
Step 1: Create the Reachability Matrix
We create a 2D boolean matrix:
reachable[i][j]
This matrix represents whether course i is a prerequisite of course j.
Initially, every value is False.
Step 2: Mark Direct Prerequisites
For every prerequisite pair [a, b]:
reachable[a][b] = True
This establishes all direct edges in the graph.
Step 3: Use Intermediate Nodes
We now attempt to improve reachability information by allowing intermediate nodes.
For every intermediate course k:
- Check every possible start course
i - Check every possible destination course
j - If:
ican reachk- and
kcan reachj
- Then:
ican also reachj
The update becomes:
reachable[i][j] |= reachable[i][k] and reachable[k][j]
This progressively computes indirect prerequisite chains.
Step 4: Answer Queries
For every query [u, v], simply return:
reachable[u][v]
This is now an O(1) lookup.
Why it works
The algorithm works because Floyd-Warshall systematically considers every node as a possible intermediate connection point.
After processing node k, the matrix correctly represents all paths that use only nodes 0..k as intermediates.
By the end, every possible path in the graph has been considered. Therefore, reachable[a][b] is true if and only if there exists a directed path from a to b.
Python Solution
from typing import List
class Solution:
def checkIfPrerequisite(
self,
numCourses: int,
prerequisites: List[List[int]],
queries: List[List[int]]
) -> List[bool]:
reachable = [
[False] * numCourses
for _ in range(numCourses)
]
# Direct prerequisites
for prereq, course in prerequisites:
reachable[prereq][course] = True
# Floyd-Warshall transitive closure
for intermediate in range(numCourses):
for start in range(numCourses):
if reachable[start][intermediate]:
for end in range(numCourses):
if reachable[intermediate][end]:
reachable[start][end] = True
# Answer queries
result = []
for u, v in queries:
result.append(reachable[u][v])
return result
The implementation begins by allocating a numCourses × numCourses boolean matrix. Each entry represents whether one course is reachable from another.
Next, we populate all direct prerequisite relationships from the input array.
The core of the algorithm is the triple nested loop. The outer loop selects an intermediate node. The inner loops determine whether a path can be improved by passing through that intermediate node.
An optimization is included:
if reachable[start][intermediate]:
There is no reason to check all destinations if the start node cannot even reach the intermediate node.
Finally, queries are answered using direct matrix lookups, which makes each query extremely fast.
Go Solution
func checkIfPrerequisite(numCourses int, prerequisites [][]int, queries [][]int) []bool {
reachable := make([][]bool, numCourses)
for i := 0; i < numCourses; i++ {
reachable[i] = make([]bool, numCourses)
}
// Direct prerequisites
for _, edge := range prerequisites {
prereq := edge[0]
course := edge[1]
reachable[prereq][course] = true
}
// Floyd-Warshall transitive closure
for intermediate := 0; intermediate < numCourses; intermediate++ {
for start := 0; start < numCourses; start++ {
if reachable[start][intermediate] {
for end := 0; end < numCourses; end++ {
if reachable[intermediate][end] {
reachable[start][end] = true
}
}
}
}
}
// Answer queries
result := make([]bool, 0, len(queries))
for _, query := range queries {
u := query[0]
v := query[1]
result = append(result, reachable[u][v])
}
return result
}
The Go implementation closely mirrors the Python version.
The primary difference is explicit slice allocation. Go requires manually constructing the 2D boolean matrix using nested make calls.
Go slices default to zero values, so all entries naturally begin as false.
No integer overflow concerns exist because all operations involve small indices and boolean values.
Worked Examples
Example 1
Input:
numCourses = 2
prerequisites = [[1,0]]
queries = [[0,1],[1,0]]
Initial Matrix
| From \ To | 0 | 1 |
|---|---|---|
| 0 | F | F |
| 1 | T | F |
This means:
1 -> 0
Processing Intermediate Node 0
No additional paths are discovered.
Processing Intermediate Node 1
No additional paths are discovered.
Final matrix:
| From \ To | 0 | 1 |
|---|---|---|
| 0 | F | F |
| 1 | T | F |
Query Results
| Query | Result |
|---|---|
| [0,1] | False |
| [1,0] | True |
Output:
[false, true]
Example 2
Input:
numCourses = 2
prerequisites = []
queries = [[1,0],[0,1]]
Initial Matrix
| From \ To | 0 | 1 |
|---|---|---|
| 0 | F | F |
| 1 | F | F |
No prerequisites exist.
No updates occur during Floyd-Warshall processing.
Query Results
| Query | Result |
|---|---|
| [1,0] | False |
| [0,1] | False |
Output:
[false, false]
Example 3
Input:
numCourses = 3
prerequisites = [[1,2],[1,0],[2,0]]
queries = [[1,0],[1,2]]
Initial Matrix
| From \ To | 0 | 1 | 2 |
|---|---|---|---|
| 0 | F | F | F |
| 1 | T | F | T |
| 2 | T | F | F |
Processing Intermediate Node 0
No new paths discovered.
Processing Intermediate Node 1
No new paths discovered.
Processing Intermediate Node 2
We already know:
1 -> 2
2 -> 0
Therefore:
1 -> 0
was already true.
Final matrix:
| From \ To | 0 | 1 | 2 |
|---|---|---|---|
| 0 | F | F | F |
| 1 | T | F | T |
| 2 | T | F | F |
Query Results
| Query | Result |
|---|---|
| [1,0] | True |
| [1,2] | True |
Output:
[true, true]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(V³ + Q) | Floyd-Warshall preprocessing plus constant-time query lookups |
| Space | O(V²) | Reachability matrix storage |
The dominant cost comes from the triple nested loops used for transitive closure computation.
Since numCourses <= 100, the maximum number of operations is approximately:
100³ = 1,000,000
which is very manageable.
The query phase is extremely efficient because every query becomes a direct matrix lookup.
Test Cases
from typing import List
class Solution:
def checkIfPrerequisite(
self,
numCourses: int,
prerequisites: List[List[int]],
queries: List[List[int]]
) -> List[bool]:
reachable = [
[False] * numCourses
for _ in range(numCourses)
]
for prereq, course in prerequisites:
reachable[prereq][course] = True
for intermediate in range(numCourses):
for start in range(numCourses):
if reachable[start][intermediate]:
for end in range(numCourses):
if reachable[intermediate][end]:
reachable[start][end] = True
return [reachable[u][v] for u, v in queries]
solution = Solution()
# Example 1
assert solution.checkIfPrerequisite(
2,
[[1, 0]],
[[0, 1], [1, 0]]
) == [False, True] # direct prerequisite relationship
# Example 2
assert solution.checkIfPrerequisite(
2,
[],
[[1, 0], [0, 1]]
) == [False, False] # completely disconnected graph
# Example 3
assert solution.checkIfPrerequisite(
3,
[[1, 2], [1, 0], [2, 0]],
[[1, 0], [1, 2]]
) == [True, True] # indirect and direct prerequisites
# Single chain
assert solution.checkIfPrerequisite(
4,
[[0, 1], [1, 2], [2, 3]],
[[0, 3], [1, 3], [3, 0]]
) == [True, True, False] # long transitive chain
# Multiple disconnected components
assert solution.checkIfPrerequisite(
6,
[[0, 1], [2, 3], [4, 5]],
[[0, 1], [0, 3], [2, 3], [4, 5]]
) == [True, False, True, True] # separate components
# Diamond dependency structure
assert solution.checkIfPrerequisite(
4,
[[0, 1], [0, 2], [1, 3], [2, 3]],
[[0, 3], [1, 2], [2, 3]]
) == [True, False, True] # multiple paths to same node
# No prerequisites
assert solution.checkIfPrerequisite(
5,
[],
[[0, 1], [1, 2], [3, 4]]
) == [False, False, False] # empty graph
# Dense graph
assert solution.checkIfPrerequisite(
4,
[[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]],
[[0,3],[1,3],[2,0]]
) == [True, True, False] # heavily connected DAG
Test Summary
| Test | Why |
|---|---|
| Example 1 | Validates direct prerequisite relationships |
| Example 2 | Validates behavior with no edges |
| Example 3 | Validates transitive prerequisites |
| Single chain | Tests long indirect dependency propagation |
| Multiple disconnected components | Ensures unrelated subgraphs remain independent |
| Diamond dependency structure | Verifies multiple prerequisite paths |
| No prerequisites | Confirms all queries return false |
| Dense graph | Stresses transitive closure updates |
Edge Cases
One important edge case is when there are no prerequisites at all. In this situation, every course is independent, and every query should return False. A buggy implementation might accidentally infer relationships from uninitialized memory or incorrect defaults. Our implementation handles this correctly because the reachability matrix starts entirely as False, and no updates occur.
Another important case is long indirect prerequisite chains. For example:
0 -> 1 -> 2 -> 3 -> 4
A naive implementation that only checks direct edges would fail to recognize that 0 is a prerequisite of 4. The Floyd-Warshall transitive closure computation correctly propagates reachability across arbitrarily long chains.
A third important edge case is disconnected graph components. Some courses may belong to entirely separate dependency trees. For example:
0 -> 1
2 -> 3
Queries between the two components should always return False. Since the algorithm only updates reachability when valid connecting paths exist, disconnected components remain isolated throughout computation.
Another subtle edge case involves multiple paths between the same nodes. Consider:
0 -> 1 -> 3
0 -> 2 -> 3
The algorithm must still correctly mark 0 -> 3 exactly once regardless of how many distinct paths exist. The boolean reachability matrix naturally handles this because once an entry becomes True, repeated discoveries do not affect correctness.