195 lines
9.8 KiB
YAML
195 lines
9.8 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
|
|
|
|
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`
|
|
|
|
|
|
|
|
**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])`
|
|
|
|
|
|
|
|
**Step 3: Answer queries**
|
|
|
|
- For each query `[u, v]`, simply look up `reachable[u][v]`
|
|
- Return the list of boolean answers
|
|
|
|
|
|
|
|
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.
|