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: - slug: bfs is_optimal: true - slug: dfs is_optimal: false - slug: dynamic-programming is_optimal: false 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 jth 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* jth *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.