{x}
blog image

Course Schedule IV

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.

  • 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] = [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
  • All the pairs [ai, bi] are unique.
  • The prerequisites graph has no cycles.
  • 1 <= queries.length <= 104
  • 0 <= ui, vi <= numCourses - 1
  • ui != vi

Solution Explanation for Course Schedule IV

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.

Approach 1: Floyd's Algorithm

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.

  1. 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.

  2. 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.

  3. 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.

Approach 2: Topological Sort

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.

  1. 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.

  2. 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.

  3. Reachability: After topological sorting, f[i][j] will be true if course i is a prerequisite for course j.

  4. 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.

Code Examples (Python)

Approach 1: Floyd's Algorithm

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]
 

Approach 2: Topological Sort

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.