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] = [ai, bi]
indicates that you must take course ai
first if you want to take course bi
.
[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] = [uj, vj]
. For the jth
query, you should answer whether course uj
is a prerequisite of course vj
or not.
Return a boolean array answer
, where answer[j]
is the answer to the jth
query.
Example 1:
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.
Example 2:
Input: numCourses = 2, prerequisites = [], queries = [[1,0],[0,1]] Output: [false,false] Explanation: There are no prerequisites, and each course is independent.
Example 3:
Input: numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]] Output: [true,true]
Constraints:
2 <= numCourses <= 100
0 <= prerequisites.length <= (numCourses * (numCourses - 1) / 2)
prerequisites[i].length == 2
0 <= ai, bi <= numCourses - 1
ai != bi
[ai, bi]
are unique.1 <= queries.length <= 104
0 <= ui, vi <= numCourses - 1
ui != vi
This problem asks whether course u
is a prerequisite for course v
, considering direct and indirect prerequisites. We can solve this using either Floyd's Algorithm or Topological Sort. Both approaches build a graph representing course dependencies and then check reachability.
Floyd's algorithm efficiently finds the shortest paths between all pairs of nodes in a graph. In this context, we adapt it to find reachability: if we can reach node v
from node u
, then u
is a prerequisite for v
.
Adjacency Matrix: We create an adjacency matrix f
of size numCourses x numCourses
. f[i][j]
will be true
if course i
is a prerequisite for course j
, and false
otherwise. Initially, we set f[i][j] = true
for all direct prerequisites given in prerequisites
.
Floyd-Warshall: We iterate through all possible intermediate courses (k
). For each pair of courses (i
, j
), if f[i][k]
and f[k][j]
are both true (meaning we can reach k
from i
and j
from k
), then we can reach j
from i
, so we set f[i][j] = true
. This step accounts for indirect prerequisites.
Querying: Finally, for each query [u, v]
, we return f[u][v]
.
Time Complexity: O(N³), where N is numCourses
. This is dominated by the three nested loops in Floyd's algorithm.
Space Complexity: O(N²), due to the adjacency matrix f
.
Topological sort arranges nodes in a directed acyclic graph (DAG) such that for every directed edge from node A to node B, node A appears before node B in the ordering. We can use this to determine reachability.
Graph Representation: We build a graph using an adjacency list (g
) and an in-degree array (indeg
). g[i]
lists the courses that can be taken after course i
, and indeg[i]
stores the number of prerequisites for course i
.
Topological Sort: We perform a topological sort using a queue. We add all courses with an in-degree of 0 to the queue. We then process each course in the queue, updating the adjacency matrix f
to reflect reachability. When a course is processed, we update its successors' in-degrees and add successors with in-degree 0 to the queue.
Reachability: After topological sorting, f[i][j]
will be true
if course i
is a prerequisite for course j
.
Querying: We answer queries using the final f
matrix, as in Approach 1.
Time Complexity: O(N + E), where N is the number of courses and E is the number of prerequisites (edges in the graph). This is the time for topological sort. However, we also have the nested loop in step 3, giving the total time as O(N³) in the worst case.
Space Complexity: O(N + E) for the adjacency list, in-degree array, and queue, but again, the adjacency matrix adds O(N²), dominating the space complexity.
class Solution:
def checkIfPrerequisite(self, numCourses, prerequisites, queries):
f = [[False] * numCourses for _ in range(numCourses)]
for a, b in prerequisites:
f[a][b] = True
for k in range(numCourses):
for i in range(numCourses):
for j in range(numCourses):
f[i][j] |= f[i][k] and f[k][j]
return [f[u][v] for u, v in queries]
from collections import deque
class Solution:
def checkIfPrerequisite(self, numCourses, prerequisites, queries):
f = [[False] * numCourses for _ in range(numCourses)]
g = [[] for _ in range(numCourses)]
indeg = [0] * numCourses
for a, b in prerequisites:
g[a].append(b)
indeg[b] += 1
q = deque([i for i, x in enumerate(indeg) if x == 0])
while q:
i = q.popleft()
for j in g[i]:
f[i][j] = True
for h in range(numCourses):
f[h][j] |= f[h][i]
indeg[j] -= 1
if indeg[j] == 0:
q.append(j)
return [f[u][v] for u, v in queries]
Both approaches achieve the same result. Floyd's algorithm is slightly simpler to implement, while topological sort might offer better performance on sparse graphs (graphs with relatively few edges compared to the number of nodes). For this problem's constraints, the difference is unlikely to be significant. Choose the approach you find more conceptually clear or easier to implement.