Before attempting this problem, you should be comfortable with:
Graph Representation (Adjacency List) - Required to model course dependencies as a directed graph
Topological Sort (Kahn's Algorithm) - Primary technique using BFS to process nodes level by level
In-degree Tracking - Understanding how to count and update incoming edges for each node
Cycle Detection in Directed Graphs - Needed to identify impossible course schedules
Depth-First Search (DFS) - Alternative approach for both cycle detection and longest path computation
1. Breadth-First Search (Kahn's Algorithm)
Intuition
This problem asks for the minimum number of semesters to complete all courses with prerequisites. Since we can take multiple courses in parallel each semester (as long as their prerequisites are met), this naturally maps to a topological sort problem where we process courses level by level.
Kahn's algorithm works perfectly here. We start with courses that have no prerequisites (in-degree of 0) and take them all in the first semester. Then we remove their outgoing edges, which may unlock new courses for the next semester. Each BFS level represents one semester.
If we cannot complete all courses (due to a cycle), we return -1.
Algorithm
Build an adjacency list and compute in-degrees for all courses.
Initialize a queue with all courses having in-degree 0 (no prerequisites).
Process the queue level by level, where each level represents a semester:
For each course in the current level, decrement the in-degree of its dependent courses.
Add any course whose in-degree becomes 0 to the next level.
Increment the semester counter.
If all courses are processed, return the number of semesters. Otherwise, return -1.
classSolution:defminimumSemesters(self, N:int, relations: List[List[int]])->int:
graph ={i:[]for i inrange(1, N +1)}
in_count ={i:0for i inrange(1, N +1)}# or in-degreefor start_node, end_node in relations:
graph[start_node].append(end_node)
in_count[end_node]+=1
queue =[]# we use list here since we are not# popping from the front in this codefor node in graph:if in_count[node]==0:
queue.append(node)
step =0
studied_count =0# start learning with BFSwhile queue:# start new semester
step +=1
next_queue =[]for node in queue:
studied_count +=1
end_nodes = graph[node]for end_node in end_nodes:
in_count[end_node]-=1# if all prerequisite courses learnedif in_count[end_node]==0:
next_queue.append(end_node)
queue = next_queue
return step if studied_count == N else-1
classSolution{publicintminimumSemesters(intN,int[][] relations){int[] inCount =newint[N+1];// or indegreeList<List<Integer>> graph =newArrayList<>(N+1);for(int i =0; i <N+1;++i){
graph.add(newArrayList<Integer>());}for(int[] relation : relations){
graph.get(relation[0]).add(relation[1]);
inCount[relation[1]]++;}int step =0;int studiedCount =0;List<Integer> bfsQueue =newArrayList<>();for(int node =1; node <N+1; node++){if(inCount[node]==0){
bfsQueue.add(node);}}// start learning with BFSwhile(!bfsQueue.isEmpty()){// start new semester
step++;List<Integer> nextQueue =newArrayList<>();for(int node : bfsQueue){
studiedCount++;for(int endNode : graph.get(node)){
inCount[endNode]--;// if all prerequisite courses learnedif(inCount[endNode]==0){
nextQueue.add(endNode);}}}
bfsQueue = nextQueue;}// check if learn all coursesreturn studiedCount ==N? step :-1;}}
classSolution{public:intminimumSemesters(int N, vector<vector<int>>& relations){
vector<int>inCount(N +1,0);// or indegree
vector<vector<int>>graph(N +1);for(auto& relation : relations){
graph[relation[0]].push_back(relation[1]);
inCount[relation[1]]++;}int step =0;int studiedCount =0;
vector<int> bfsQueue;for(int node =1; node < N +1; node++){if(inCount[node]==0){
bfsQueue.push_back(node);}}// start learning with BFSwhile(!bfsQueue.empty()){// start new semester
step++;
vector<int> nextQueue;for(auto& node : bfsQueue){
studiedCount++;for(auto& endNode : graph[node]){
inCount[endNode]--;// if all prerequisite courses learnedif(inCount[endNode]==0){
nextQueue.push_back(endNode);}}}
bfsQueue = nextQueue;}// check if learn all coursesreturn studiedCount == N ? step :-1;}};
classSolution{/**
* @param {number} n
* @param {number[][]} relations
* @return {number}
*/minimumSemesters(n, relations){const graph ={};const inCount ={};// or in-degreefor(let i =1; i <= n; i++){
graph[i]=[];
inCount[i]=0;}for(const[startNode, endNode]of relations){
graph[startNode].push(endNode);
inCount[endNode]++;}let queue =[];// we use list here since we are not// popping from the front in this codefor(let node =1; node <= n; node++){if(inCount[node]===0){
queue.push(node);}}let step =0;let studiedCount =0;// start learning with BFSwhile(queue.length >0){// start new semester
step++;const nextQueue =[];for(const node of queue){
studiedCount++;const endNodes = graph[node];for(const endNode of endNodes){
inCount[endNode]--;// if all prerequisite courses learnedif(inCount[endNode]===0){
nextQueue.push(endNode);}}}
queue = nextQueue;}return studiedCount === n ? step :-1;}}
class Solution {funminimumSemesters(n: Int, relations: Array<IntArray>): Int {val graph =Array(n +1){ mutableListOf<Int>()}val inCount =IntArray(n +1)for(relation in relations){
graph[relation[0]].add(relation[1])
inCount[relation[1]]++}var step =0var studiedCount =0var queue = mutableListOf<Int>()for(node in1..n){if(inCount[node]==0){
queue.add(node)}}while(queue.isNotEmpty()){
step++val nextQueue = mutableListOf<Int>()for(node in queue){
studiedCount++for(endNode in graph[node]){
inCount[endNode]--if(inCount[endNode]==0){
nextQueue.add(endNode)}}}
queue = nextQueue
}returnif(studiedCount == n) step else-1}}
classSolution{funcminimumSemesters(_ n:Int,_ relations:[[Int]])->Int{var graph =[[Int]](repeating:[], count: n +1)var inCount =[Int](repeating:0, count: n +1)for relation in relations {
graph[relation[0]].append(relation[1])
inCount[relation[1]]+=1}var step =0var studiedCount =0var queue =[Int]()for node in1...n {if inCount[node]==0{
queue.append(node)}}while!queue.isEmpty {
step +=1var nextQueue =[Int]()for node in queue {
studiedCount +=1for endNode in graph[node]{
inCount[endNode]-=1if inCount[endNode]==0{
nextQueue.append(endNode)}}}
queue = nextQueue
}return studiedCount == n ? step :-1}}
Time & Space Complexity
Time complexity: O(N+E)
Space complexity: O(N+E)
Where E is the length of relations and N is the number of courses.
2. Depth-First Search: Check for Cycles + Find Longest Path
Intuition
The minimum number of semesters equals the length of the longest path in the prerequisite graph. This is because courses along the longest dependency chain must be taken sequentially, one per semester.
This approach uses two DFS passes: first to detect cycles (making it impossible to finish), then to find the longest path from any starting node. We use memoization to avoid recomputing path lengths for nodes we have already visited.
Algorithm
Build an adjacency list from the relations.
First dfs pass: detect cycles using three states (unvisited, visiting, visited).
If we encounter a node in the "visiting" state, a cycle exists.
Return -1 if any cycle is found.
Second dfs pass: compute the longest path starting from each node.
For each node, recursively find the maximum path length through its neighbors.
classSolution:defminimumSemesters(self, N:int, relations: List[List[int]])->int:
graph ={i:[]for i inrange(1, N +1)}for start_node, end_node in relations:
graph[start_node].append(end_node)# check if the graph contains a cycle
visited ={}defdfs_check_cycle(node:int)->bool:# return True if graph has a cycleif node in visited:return visited[node]else:# mark as visiting
visited[node]=-1for end_node in graph[node]:if dfs_check_cycle(end_node):# we meet a cycle!returnTrue# mark as visited
visited[node]=FalsereturnFalse# if has cycle, return -1for node in graph.keys():if dfs_check_cycle(node):return-1# if no cycle, return the longest path
visited_length ={}defdfs_max_path(node:int)->int:# return the longest path (inclusive)if node in visited_length:return visited_length[node]
max_length =1for end_node in graph[node]:
length = dfs_max_path(end_node)
max_length =max(length+1, max_length)# store it
visited_length[node]= max_length
return max_length
returnmax(dfs_max_path(node)for node in graph.keys())
classSolution{publicintminimumSemesters(intN,int[][] relations){List<List<Integer>> graph =newArrayList<>(N+1);for(int i =0; i <N+1;++i){
graph.add(newArrayList<Integer>());}for(int[] relation : relations){
graph.get(relation[0]).add(relation[1]);}// check if the graph contains a cycleint[] visited =newint[N+1];for(int node =1; node <N+1; node++){// if has cycle, return -1if(dfsCheckCycle(node, graph, visited)==-1){return-1;}}// if no cycle, return the longest pathint[] visitedLength =newint[N+1];int maxLength =1;for(int node =1; node <N+1; node++){int length =dfsMaxPath(node, graph, visitedLength);
maxLength =Math.max(length, maxLength);}return maxLength;}privateintdfsCheckCycle(int node,List<List<Integer>> graph,int[] visited){// return -1 if has a cycle// return 1 if does not have any cycleif(visited[node]!=0){return visited[node];}else{// mark as visiting
visited[node]=-1;}for(int endNode : graph.get(node)){if(dfsCheckCycle(endNode, graph, visited)==-1){// we meet a cycle!return-1;}}// mark as visited
visited[node]=1;return1;}privateintdfsMaxPath(int node,List<List<Integer>> graph,int[] visitedLength){// return the longest path (inclusive)if(visitedLength[node]!=0){return visitedLength[node];}int maxLength =1;for(int endNode : graph.get(node)){int length =dfsMaxPath(endNode, graph, visitedLength);
maxLength =Math.max(length +1, maxLength);}// store it
visitedLength[node]= maxLength;return maxLength;}}
classSolution{public:intminimumSemesters(int N, vector<vector<int>>& relations){
vector<vector<int>>graph(N +1);for(auto& relation : relations){
graph[relation[0]].push_back(relation[1]);}// check if the graph contains a cycle
vector<int>visited(N +1,0);for(int node =1; node < N +1; node++){// if has cycle, return -1if(dfsCheckCycle(node, graph, visited)==-1){return-1;}}// if no cycle, return the longest path
vector<int>visitedLength(N +1,0);int maxLength =1;for(int node =1; node < N +1; node++){int length =dfsMaxPath(node, graph, visitedLength);
maxLength =max(length, maxLength);}return maxLength;}private:intdfsCheckCycle(int node, vector<vector<int>>& graph,
vector<int>& visited){// return -1 if has a cycle// return 1 if does not have any cycleif(visited[node]!=0){return visited[node];}else{// mark as visiting
visited[node]=-1;}for(auto& endNode : graph[node]){if(dfsCheckCycle(endNode, graph, visited)==-1){// we meet a cycle!return-1;}}// mark as visited
visited[node]=1;return1;}intdfsMaxPath(int node, vector<vector<int>>& graph,
vector<int>& visitedLength){// return the longest path (inclusive)if(visitedLength[node]!=0){return visitedLength[node];}int maxLength =1;for(auto& endNode : graph[node]){int length =dfsMaxPath(endNode, graph, visitedLength);
maxLength =max(length +1, maxLength);}// store it
visitedLength[node]= maxLength;return maxLength;}};
Where E is the length of relations and N is the number of courses.
3. Depth-First Search: Combine
Intuition
We can combine cycle detection and longest path computation into a single DFS. The key insight is that a node in the "visiting" state during DFS indicates a cycle. We use -1 as a sentinel value to indicate both "currently visiting" and "cycle detected."
When we finish processing a node, we store its longest path length. If we ever encounter -1 from a recursive call, we propagate that cycle indicator upward. This eliminates the need for a separate cycle detection pass.
Algorithm
Build an adjacency list from the relations.
Run dfs from each unvisited node:
Mark the node as visiting (-1).
Recursively compute the longest path through neighbors.
If any neighbor returns -1, propagate the cycle indicator.
Store the computed path length and return it.
Track the maximum path length across all nodes.
Return -1 if a cycle was detected, otherwise return the maximum path length.
classSolution:defminimumSemesters(self, N:int, relations: List[List[int]])->int:
graph ={i:[]for i inrange(1, N +1)}for start_node, end_node in relations:
graph[start_node].append(end_node)
visited ={}defdfs(node:int)->int:# return the longest path (inclusive)if node in visited:return visited[node]else:# mark as visiting
visited[node]=-1
max_length =1for end_node in graph[node]:
length = dfs(end_node)# we meet a cycle!if length ==-1:return-1else:
max_length =max(length+1, max_length)# mark as visited
visited[node]= max_length
return max_length
max_length =-1for node in graph.keys():
length = dfs(node)# we meet a cycle!if length ==-1:return-1else:
max_length =max(length, max_length)return max_length
classSolution{publicintminimumSemesters(intN,int[][] relations){List<List<Integer>> graph =newArrayList<>(N+1);for(int i =0; i <N+1;++i){
graph.add(newArrayList<Integer>());}for(int[] relation : relations){
graph.get(relation[0]).add(relation[1]);}int[] visited =newint[N+1];int maxLength =1;for(int node =1; node <N+1; node++){int length =dfs(node, graph, visited);// we meet a cycle!if(length ==-1){return-1;}
maxLength =Math.max(length, maxLength);}return maxLength;}privateintdfs(int node,List<List<Integer>> graph,int[] visited){// return the longest path (inclusive)if(visited[node]!=0){return visited[node];}else{// mark as visiting
visited[node]=-1;}int maxLength =1;for(int endNode : graph.get(node)){int length =dfs(endNode, graph, visited);// we meet a cycle!if(length ==-1){return-1;}
maxLength =Math.max(length +1, maxLength);}// mark as visited
visited[node]= maxLength;return maxLength;}}
classSolution{public:intminimumSemesters(int N, vector<vector<int>>& relations){
vector<vector<int>>graph(N +1);for(auto& relation : relations){
graph[relation[0]].push_back(relation[1]);}
vector<int>visited(N +1,0);int maxLength =1;for(int node =1; node < N +1; node++){int length =dfs(node, graph, visited);// we meet a cycle!if(length ==-1){return-1;}
maxLength =max(length, maxLength);}return maxLength;}private:intdfs(int node, vector<vector<int>>& graph, vector<int>& visited){// return the longest path (inclusive)if(visited[node]!=0){return visited[node];}else{// mark as visiting
visited[node]=-1;}int maxLength =1;for(auto& endNode : graph[node]){int length =dfs(endNode, graph, visited);// we meet a cycle!if(length ==-1){return-1;}
maxLength =max(length +1, maxLength);}// mark as visited
visited[node]= maxLength;return maxLength;}};
Where E is the length of relations and N is the number of courses.
Common Pitfalls
Forgetting to Detect Cycles
If the prerequisite graph contains a cycle, it is impossible to complete all courses. Returning the longest path length without first checking for cycles will give a wrong answer. Always verify that all courses can be processed (studied count equals N) before returning the result.
Counting Courses Instead of Semesters in BFS
With Kahn's algorithm, each BFS level represents one semester, not one course. The answer is the number of levels traversed, not the total number of courses processed. Incrementing the counter for each course instead of each level gives an incorrect result.
Using the Wrong DFS State Values
When combining cycle detection with longest path calculation, using -1 as both "visiting" and "cycle detected" requires careful handling. If a node returns -1, you must propagate this upward immediately rather than continuing to compute path lengths.