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.
Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Graph Representation - Building adjacency lists from edge pairs to represent directed graphs
Depth-First Search (DFS) - Recursive graph traversal for path finding and reachability queries
Topological Sort (Kahn's Algorithm) - BFS-based approach using indegree to process nodes in dependency order
Memoization - Caching computed results to avoid redundant DFS traversals
Floyd-Warshall Algorithm - Dynamic programming approach for computing transitive closure in O(V^3) time
1. Brute Force (DFS)
Intuition
To check if course A is a prerequisite of course B, we need to determine if there is a path from A to B in the prerequisite graph. A depth-first search starting from A can explore all courses reachable from A. If we reach B during this traversal, then A is indeed a prerequisite of B.
Algorithm
Build an adjacency list from the prerequisites, where each course points to its direct successors.
For each query (u, v), run a DFS starting from u.
In the DFS, if we reach v, return true.
Otherwise, recursively explore all neighbors and return true if any path reaches v.
classSolution:defcheckIfPrerequisite(self, numCourses:int, prerequisites: List[List[int]], queries: List[List[int]])-> List[bool]:
adj =[[]for _ inrange(numCourses)]for u, v in prerequisites:
adj[u].append(v)defdfs(node, target):if node == target:returnTruefor nei in adj[node]:if dfs(nei, target):returnTruereturnFalse
res =[]for u, v in queries:
res.append(dfs(u, v))return res
publicclassSolution{privateList<Integer>[] adj;publicList<Boolean>checkIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){
adj =newArrayList[numCourses];for(int i =0; i < numCourses; i++) adj[i]=newArrayList<>();for(int[] pre : prerequisites) adj[pre[0]].add(pre[1]);List<Boolean> res =newArrayList<>();for(int[] query : queries){
res.add(dfs(query[0], query[1]));}return res;}privatebooleandfs(int node,int target){if(node == target)returntrue;for(int nei : adj[node]){if(dfs(nei, target))returntrue;}returnfalse;}}
publicclassSolution{publicList<bool>CheckIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){List<int>[] adj =newList<int>[numCourses];for(int i =0; i < numCourses; i++){
adj[i]=newList<int>();}foreach(var pre in prerequisites){
adj[pre[0]].Add(pre[1]);}boolDfs(int node,int target){if(node == target)returntrue;foreach(var nei in adj[node]){if(Dfs(nei, target))returntrue;}returnfalse;}var res =newList<bool>();foreach(var q in queries){
res.Add(Dfs(q[0], q[1]));}return res;}}
funccheckIfPrerequisite(numCourses int, prerequisites [][]int, queries [][]int)[]bool{
adj :=make([][]int, numCourses)for i :=range adj {
adj[i]=[]int{}}for_, pre :=range prerequisites {
adj[pre[0]]=append(adj[pre[0]], pre[1])}var dfs func(node, target int)bool
dfs =func(node, target int)bool{if node == target {returntrue}for_, nei :=range adj[node]{ifdfs(nei, target){returntrue}}returnfalse}
res :=make([]bool,len(queries))for i, q :=range queries {
res[i]=dfs(q[0], q[1])}return res
}
class Solution {privatelateinitvar adj: Array<MutableList<Int>>funcheckIfPrerequisite(numCourses: Int, prerequisites: Array<IntArray>, queries: Array<IntArray>): List<Boolean>{
adj =Array(numCourses){mutableListOf()}for(pre in prerequisites){
adj[pre[0]].add(pre[1])}return queries.map{dfs(it[0], it[1])}}privatefundfs(node: Int, target: Int): Boolean {if(node == target)returntruefor(nei in adj[node]){if(dfs(nei, target))returntrue}returnfalse}}
classSolution{privatevar adj =[[Int]]()funccheckIfPrerequisite(_ numCourses:Int,_ prerequisites:[[Int]],_ queries:[[Int]])->[Bool]{
adj =Array(repeating:[Int](), count: numCourses)for pre in prerequisites {
adj[pre[0]].append(pre[1])}funcdfs(_ node:Int,_ target:Int)->Bool{if node == target {returntrue}for nei in adj[node]{ifdfs(nei, target){returntrue}}returnfalse}return queries.map {dfs($0[0],$0[1])}}}
Time & Space Complexity
Time complexity: O((V+E)∗m)
Space complexity: O(V+E+m)
Where m is the number of queries, V is the number of courses, and E is the number of prerequisites.
2. Depth First Search (Hash Set)
Intuition
Instead of running DFS for every query, we can precompute all prerequisites for each course. For each course, we use DFS to find all courses that are prerequisites (directly or indirectly) and store them in a set. Then answering any query becomes a simple set lookup.
Algorithm
Build an adjacency list where each course points to its direct prerequisites.
For each course, run DFS to collect all reachable prerequisites into a set.
Cache these sets to avoid recomputation.
Each course's prerequisite set includes itself and all prerequisites of its direct prerequisites.
For each query (u, v), check if u is in the prerequisite set of v.
Where m is the number of queries, V is the number of courses, and E is the number of prerequisites.
3. Depth First Search (Memoization)
Intuition
We can optimize by memoizing the result for each pair of courses. When checking if course A is a prerequisite of course B, we store the result so that future queries for the same pair can be answered instantly. This avoids redundant graph traversals for repeated or similar queries.
Algorithm
Build an adjacency list where each course points to its direct prerequisites.
Create a 2D memoization array initialized to -1 (unknown).
For each query (u, v), run DFS to check if u is a prerequisite of v.
During DFS, if the result for (course, prereq) is already computed, return it.
Otherwise, check all direct prerequisites. If any of them is the target or leads to the target, mark and return true.
classSolution:defcheckIfPrerequisite(self, numCourses:int, prerequisites: List[List[int]], queries: List[List[int]])-> List[bool]:
adj =[[]for _ inrange(numCourses)]
isPrereq =[[-1]* numCourses for _ inrange(numCourses)]for prereq, crs in prerequisites:
adj[crs].append(prereq)
isPrereq[crs][prereq]=Truedefdfs(crs, prereq):if isPrereq[crs][prereq]!=-1:return isPrereq[crs][prereq]==1for pre in adj[crs]:if pre == prereq or dfs(pre, prereq):
isPrereq[crs][prereq]=1returnTrue
isPrereq[crs][prereq]=0returnFalse
res =[]for u, v in queries:
res.append(dfs(v, u))return res
publicclassSolution{privateList<Integer>[] adj;privateint[][] isPrereq;publicList<Boolean>checkIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){
adj =newArrayList[numCourses];
isPrereq =newint[numCourses][numCourses];for(int i =0; i < numCourses; i++){
adj[i]=newArrayList<>();Arrays.fill(isPrereq[i],-1);}for(int[] pre : prerequisites){
adj[pre[1]].add(pre[0]);
isPrereq[pre[1]][pre[0]]=1;}List<Boolean> res =newArrayList<>();for(int[] query : queries){
res.add(dfs(query[1], query[0]));}return res;}privatebooleandfs(int crs,int prereq){if(isPrereq[crs][prereq]!=-1){return isPrereq[crs][prereq]==1;}for(int pre : adj[crs]){if(pre == prereq ||dfs(pre, prereq)){
isPrereq[crs][prereq]=1;returntrue;}}
isPrereq[crs][prereq]=0;returnfalse;}}
classSolution{privatevar adj =[[Int]]()privatevar isPrereq =[[Int]]()funccheckIfPrerequisite(_ numCourses:Int,_ prerequisites:[[Int]],_ queries:[[Int]])->[Bool]{
adj =Array(repeating:[Int](), count: numCourses)
isPrereq =Array(repeating:Array(repeating:-1, count: numCourses), count: numCourses)for pre in prerequisites {let prereq = pre[0], crs = pre[1]
adj[crs].append(prereq)
isPrereq[crs][prereq]=1}funcdfs(_ crs:Int,_ prereq:Int)->Bool{if isPrereq[crs][prereq]!=-1{return isPrereq[crs][prereq]==1}for pre in adj[crs]{if pre == prereq ||dfs(pre, prereq){
isPrereq[crs][prereq]=1returntrue}}
isPrereq[crs][prereq]=0returnfalse}return queries.map {dfs($0[1],$0[0])}}}
Time & Space Complexity
Time complexity: O(V∗(V+E)+m)
Space complexity: O(V2+E+m)
Where m is the number of queries, V is the number of courses, and E is the number of prerequisites.
4. Topological Sort (Kahn's Algorithm)
Intuition
Using topological sort, we process courses in an order where all prerequisites of a course are processed before the course itself. When we process a course, we propagate all its prerequisites to its successors. This way, each course accumulates the complete set of all courses that must be taken before it.
Algorithm
Build an adjacency list and compute the indegree for each course.
Initialize a queue with all courses having indegree0.
For each course processed:
For each successor, add the current course and all its prerequisites to the successor's prerequisite set.
Decrement the successor's indegree and add to queue if it becomes 0.
After processing all courses, each course has a complete set of its prerequisites.
For each query (u, v), check if u is in the prerequisite set of v.
publicclassSolution{publicList<bool>CheckIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){List<HashSet<int>> adj =newList<HashSet<int>>();List<HashSet<int>> isPrereq =newList<HashSet<int>>();int[] indegree =newint[numCourses];for(int i =0; i < numCourses; i++){
adj.Add(newHashSet<int>());
isPrereq.Add(newHashSet<int>());}foreach(var pair in prerequisites){int pre = pair[0], crs = pair[1];
adj[pre].Add(crs);
indegree[crs]++;}Queue<int> q =newQueue<int>();for(int i =0; i < numCourses; i++){if(indegree[i]==0){
q.Enqueue(i);}}while(q.Count >0){int node = q.Dequeue();foreach(int neighbor in adj[node]){
isPrereq[neighbor].Add(node);foreach(int p in isPrereq[node]){
isPrereq[neighbor].Add(p);}
indegree[neighbor]--;if(indegree[neighbor]==0){
q.Enqueue(neighbor);}}}List<bool> result =newList<bool>();foreach(var query in queries){int u = query[0], v = query[1];
result.Add(isPrereq[v].Contains(u));}return result;}}
funccheckIfPrerequisite(numCourses int, prerequisites [][]int, queries [][]int)[]bool{
adj :=make([]map[int]bool, numCourses)
isPrereq :=make([]map[int]bool, numCourses)
indegree :=make([]int, numCourses)for i :=0; i < numCourses; i++{
adj[i]=make(map[int]bool)
isPrereq[i]=make(map[int]bool)}for_, pre :=range prerequisites {
adj[pre[0]][pre[1]]=true
indegree[pre[1]]++}
q :=[]int{}for i :=0; i < numCourses; i++{if indegree[i]==0{
q =append(q, i)}}forlen(q)>0{
node := q[0]
q = q[1:]for neighbor :=range adj[node]{
isPrereq[neighbor][node]=truefor p :=range isPrereq[node]{
isPrereq[neighbor][p]=true}
indegree[neighbor]--if indegree[neighbor]==0{
q =append(q, neighbor)}}}
res :=make([]bool,len(queries))for i, query :=range queries {
res[i]= isPrereq[query[1]][query[0]]}return res
}
class Solution {funcheckIfPrerequisite(numCourses: Int, prerequisites: Array<IntArray>, queries: Array<IntArray>): List<Boolean>{val adj =Array(numCourses){ mutableSetOf<Int>()}val isPrereq =Array(numCourses){ mutableSetOf<Int>()}val indegree =IntArray(numCourses)for(pre in prerequisites){
adj[pre[0]].add(pre[1])
indegree[pre[1]]++}val q = ArrayDeque<Int>()for(i in0 until numCourses){if(indegree[i]==0) q.add(i)}while(q.isNotEmpty()){val node = q.removeFirst()for(neighbor in adj[node]){
isPrereq[neighbor].add(node)
isPrereq[neighbor].addAll(isPrereq[node])
indegree[neighbor]--if(indegree[neighbor]==0){
q.add(neighbor)}}}return queries.map{ isPrereq[it[1]].contains(it[0])}}}
Where m is the number of queries, V is the number of courses, and E is the number of prerequisites.
5. Floyd Warshall Algorithm
Intuition
The Floyd-Warshall algorithm finds all-pairs reachability in a graph. We can adapt it to find transitive closure: if there is a path from A to B through any intermediate course K, then A is a prerequisite of B. After running the algorithm, we have direct O(1) lookup for any pair.
Algorithm
Create a 2D boolean matrix initialized to false.
Mark direct prerequisites as true in the matrix.
For each intermediate course k, iterate through all pairs (i, j).
If there is a path from i to k and from k to j, mark the path from i to j as true.
For each query (u, v), simply return the value at matrix[u][v].
classSolution:defcheckIfPrerequisite(self, numCourses:int, prerequisites: List[List[int]], queries: List[List[int]])-> List[bool]:
res =[]
adj =[[False]* numCourses for _ inrange(numCourses)]for pre, crs in prerequisites:
adj[pre][crs]=Truefor k inrange(numCourses):for i inrange(numCourses):for j inrange(numCourses):
adj[i][j]= adj[i][j]or(adj[i][k]and adj[k][j])for u, v in queries:
res.append(adj[u][v])return res
publicclassSolution{publicList<Boolean>checkIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){boolean[][] adj =newboolean[numCourses][numCourses];List<Boolean> res =newArrayList<>();for(int[] pre : prerequisites){
adj[pre[0]][pre[1]]=true;}for(int k =0; k < numCourses; k++){for(int i =0; i < numCourses; i++){for(int j =0; j < numCourses; j++){
adj[i][j]= adj[i][j]||(adj[i][k]&& adj[k][j]);}}}for(int[] q : queries){
res.add(adj[q[0]][q[1]]);}return res;}}
classSolution{public:
vector<bool>checkIfPrerequisite(int numCourses, vector<vector<int>>& prerequisites, vector<vector<int>>& queries){
vector<vector<bool>>adj(numCourses,vector<bool>(numCourses,false));
vector<bool> res;for(auto& pre : prerequisites){
adj[pre[0]][pre[1]]=true;}for(int k =0; k < numCourses; k++){for(int i =0; i < numCourses; i++){for(int j =0; j < numCourses; j++){
adj[i][j]= adj[i][j]||(adj[i][k]&& adj[k][j]);}}}for(auto& q : queries){
res.push_back(adj[q[0]][q[1]]);}return res;}};
publicclassSolution{publicList<bool>CheckIfPrerequisite(int numCourses,int[][] prerequisites,int[][] queries){bool[,] adj =newbool[numCourses, numCourses];foreach(var pair in prerequisites){int pre = pair[0], crs = pair[1];
adj[pre, crs]=true;}for(int k =0; k < numCourses; k++){for(int i =0; i < numCourses; i++){for(int j =0; j < numCourses; j++){
adj[i, j]= adj[i, j]||(adj[i, k]&& adj[k, j]);}}}List<bool> res =newList<bool>();foreach(var query in queries){int u = query[0], v = query[1];
res.Add(adj[u, v]);}return res;}}
funccheckIfPrerequisite(numCourses int, prerequisites [][]int, queries [][]int)[]bool{
adj :=make([][]bool, numCourses)for i :=range adj {
adj[i]=make([]bool, numCourses)}for_, pre :=range prerequisites {
adj[pre[0]][pre[1]]=true}for k :=0; k < numCourses; k++{for i :=0; i < numCourses; i++{for j :=0; j < numCourses; j++{
adj[i][j]= adj[i][j]||(adj[i][k]&& adj[k][j])}}}
res :=make([]bool,len(queries))for i, q :=range queries {
res[i]= adj[q[0]][q[1]]}return res
}
class Solution {funcheckIfPrerequisite(numCourses: Int, prerequisites: Array<IntArray>, queries: Array<IntArray>): List<Boolean>{val adj =Array(numCourses){BooleanArray(numCourses)}for(pre in prerequisites){
adj[pre[0]][pre[1]]=true}for(k in0 until numCourses){for(i in0 until numCourses){for(j in0 until numCourses){
adj[i][j]= adj[i][j]||(adj[i][k]&& adj[k][j])}}}return queries.map{ adj[it[0]][it[1]]}}}
classSolution{funccheckIfPrerequisite(_ numCourses:Int,_ prerequisites:[[Int]],_ queries:[[Int]])->[Bool]{var adj =Array(repeating:Array(repeating:false, count: numCourses), count: numCourses)for pre in prerequisites {
adj[pre[0]][pre[1]]=true}for k in0..<numCourses {for i in0..<numCourses {for j in0..<numCourses {
adj[i][j]= adj[i][j]||(adj[i][k]&& adj[k][j])}}}return queries.map { adj[$0[0]][$0[1]]}}}
Time & Space Complexity
Time complexity: O(V3+E+m)
Space complexity: O(V2+E+m)
Where m is the number of queries, V is the number of courses, and E is the number of prerequisites.
Common Pitfalls
Confusing Edge Direction
A common mistake is building the graph with edges pointing in the wrong direction. The prerequisite [a, b] means course a must be taken before course b, so a -> b. Reversing this leads to incorrect reachability results.
# Wrong: edge points from course to prerequisitefor pre, crs in prerequisites:
adj[crs].append(pre)# Incorrect for "is a prerequisite of" queries# Correct: edge points from prerequisite to coursefor pre, crs in prerequisites:
adj[pre].append(crs)# Now adj[u] contains courses that u is a prerequisite of
Rerunning DFS for Every Query Without Memoization
Running a fresh DFS for each query without caching results leads to repeated work. With many queries, this causes TLE. Always memoize visited pairs or precompute the full transitive closure.
# Inefficient: no memoizationdefdfs(node, target):if node == target:returnTruefor nei in adj[node]:if dfs(nei, target):returnTruereturnFalse# Each query starts from scratch - O(V+E) per query
Not Handling Disconnected Nodes
Courses with no prerequisites and no dependents are still valid nodes. Forgetting to initialize them in your adjacency list or prerequisite map causes KeyError or incorrect results when they appear in queries.