Files
codetutor/backend/data/questions/course-schedule-iv.yaml

219 lines
11 KiB
YAML

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
function_signature: "def check_if_prerequisite(num_courses: int, prerequisites: list[list[int]], queries: list[list[int]]) -> list[bool]:"
test_cases:
visible:
- input: { num_courses: 2, prerequisites: [[1, 0]], queries: [[0, 1], [1, 0]] }
expected: [false, true]
- input: { num_courses: 2, prerequisites: [], queries: [[1, 0], [0, 1]] }
expected: [false, false]
- input: { num_courses: 3, prerequisites: [[1, 2], [1, 0], [2, 0]], queries: [[1, 0], [1, 2]] }
expected: [true, true]
hidden:
- input: { num_courses: 3, prerequisites: [[0, 1], [1, 2]], queries: [[0, 2]] }
expected: [true]
- input: { num_courses: 4, prerequisites: [[0, 1], [1, 2], [2, 3]], queries: [[0, 3], [3, 0], [1, 3]] }
expected: [true, false, true]
- input: { num_courses: 5, prerequisites: [], queries: [[0, 1], [2, 3], [4, 0]] }
expected: [false, false, false]
- input: { num_courses: 4, prerequisites: [[0, 1], [0, 2], [1, 3], [2, 3]], queries: [[0, 3], [1, 2]] }
expected: [true, false]
- input: { num_courses: 3, prerequisites: [[0, 1]], queries: [[0, 1], [1, 0], [0, 2], [2, 1]] }
expected: [true, false, false, false]
- input: { num_courses: 6, prerequisites: [[0, 1], [1, 2], [3, 4], [4, 5]], queries: [[0, 2], [3, 5], [0, 5], [2, 5]] }
expected: [true, true, false, false]
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.