1136. Parallel Courses π
Problem Description
You have n
courses numbered from 1
to n
that you need to take. Some courses have prerequisites - you must complete certain courses before you can take others.
The prerequisite relationships are given as an array relations
, where each element relations[i] = [prevCoursei, nextCoursei]
means you must complete course prevCoursei
before taking course nextCoursei
.
In each semester, you can take as many courses as you want, as long as you've already completed all their prerequisites in previous semesters.
Your task is to find the minimum number of semesters needed to complete all n
courses. If it's impossible to complete all courses (for example, due to circular dependencies), return -1
.
Example scenarios:
- If you have 3 courses and course 1 must be taken before course 2, and course 2 before course 3, you'd need 3 semesters (one course per semester due to the chain of dependencies).
- If you have 3 courses where course 1 must be taken before both courses 2 and 3, you'd need 2 semesters (take course 1 in semester 1, then courses 2 and 3 together in semester 2).
- If there's a circular dependency (course 1 requires course 2, and course 2 requires course 1), it's impossible to complete all courses, so return
-1
.
The solution uses topological sorting with BFS to process courses level by level, where each level represents one semester. Courses with no prerequisites can be taken immediately, and as courses are completed, they unlock other courses for future semesters.
Intuition
The key insight is to recognize this as a graph problem where courses are nodes and prerequisites form directed edges. We need to find the longest path from any starting course to its final dependent course - this represents the minimum number of semesters needed.
Think about how you'd naturally plan your course schedule: you'd first identify all courses with no prerequisites - these can be taken immediately in semester 1. After completing these courses, some new courses become available (their prerequisites are now satisfied). These newly available courses can be taken in semester 2, and so on.
This naturally leads us to a level-by-level processing approach, which is essentially BFS on a directed acyclic graph (DAG). Each "level" in our BFS traversal represents one semester.
The challenge is detecting if it's impossible to complete all courses. This happens when there's a cycle in the prerequisite relationships - for example, if course A requires B, B requires C, and C requires A. In such cases, none of these courses can ever be taken.
We can detect this using topological sorting with in-degrees. The idea is:
- Track how many prerequisites each course has (in-degree)
- Start with courses that have zero prerequisites (in-degree = 0)
- As we "complete" each course, we reduce the in-degree of courses that depend on it
- When a course's in-degree reaches 0, it becomes available for the next semester
- If we process all courses this way, we can complete them all; if some courses remain unprocessed (their in-degree never reached 0), there must be a cycle
The number of "rounds" of BFS (where each round processes all currently available courses) equals the minimum number of semesters needed.
Learn more about Graph and Topological Sort patterns.
Solution Approach
The solution implements topological sorting using BFS (Kahn's algorithm) to determine the minimum number of semesters needed.
Step 1: Build the graph and calculate in-degrees
g = defaultdict(list)
indeg = [0] * n
for prev, nxt in relations:
prev, nxt = prev - 1, nxt - 1 # Convert to 0-indexed
g[prev].append(nxt)
indeg[nxt] += 1
- Create an adjacency list
g
to represent the prerequisite relationships - Maintain an
indeg
array whereindeg[i]
counts how many prerequisites coursei
has - Convert course numbers from 1-indexed to 0-indexed for easier array manipulation
Step 2: Initialize the queue with courses having no prerequisites
q = deque(i for i, v in enumerate(indeg) if v == 0)
- Find all courses with in-degree 0 (no prerequisites)
- These courses can be taken in the first semester
Step 3: Process courses level by level (semester by semester)
ans = 0
while q:
ans += 1 # Start a new semester
for _ in range(len(q)): # Process all courses available this semester
i = q.popleft()
n -= 1 # One course completed
for j in g[i]:
indeg[j] -= 1
if indeg[j] == 0:
q.append(j)
- Each iteration of the outer
while
loop represents one semester - The inner
for
loop processes all courses that can be taken in the current semester - For each completed course
i
:- Decrement
n
to track remaining courses - Reduce the in-degree of all courses that depend on
i
- If any course's in-degree becomes 0, it's available for the next semester
- Decrement
Step 4: Check if all courses were completed
return -1 if n else ans
- If
n > 0
, some courses couldn't be completed (cycle exists), return-1
- Otherwise, return
ans
- the number of semesters needed
The time complexity is O(V + E)
where V
is the number of courses and E
is the number of prerequisite relationships. The space complexity is O(V + E)
for storing the graph and in-degree array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Example Input:
n = 5
courses (numbered 1 to 5)relations = [[1,3], [2,3], [3,4], [3,5]]
This means:
- Course 1 must be taken before Course 3
- Course 2 must be taken before Course 3
- Course 3 must be taken before Course 4
- Course 3 must be taken before Course 5
Step 1: Build the graph and calculate in-degrees
After converting to 0-indexed and building our data structures:
- Graph
g
:0 β [2]
(Course 1 β Course 3)1 β [2]
(Course 2 β Course 3)2 β [3, 4]
(Course 3 β Courses 4, 5)
- In-degrees
indeg = [0, 0, 2, 1, 1]
- Courses 1 and 2: no prerequisites (in-degree = 0)
- Course 3: requires Courses 1 and 2 (in-degree = 2)
- Courses 4 and 5: each requires Course 3 (in-degree = 1)
Step 2: Initialize queue with courses having no prerequisites
Queue q = [0, 1]
(Courses 1 and 2 can be taken immediately)
Step 3: Process semester by semester
Semester 1 (ans = 1
):
- Process Course 0 (Course 1):
- Remove from queue, decrement
n
to 4 - Update Course 2's in-degree:
2 - 1 = 1
- Remove from queue, decrement
- Process Course 1 (Course 2):
- Remove from queue, decrement
n
to 3 - Update Course 2's in-degree:
1 - 1 = 0
- Course 2's in-degree is now 0, add to queue
- Remove from queue, decrement
- Queue after Semester 1:
[2]
Semester 2 (ans = 2
):
- Process Course 2 (Course 3):
- Remove from queue, decrement
n
to 2 - Update Course 3's in-degree:
1 - 1 = 0
- Update Course 4's in-degree:
1 - 1 = 0
- Both Courses 3 and 4 now have in-degree 0, add to queue
- Remove from queue, decrement
- Queue after Semester 2:
[3, 4]
Semester 3 (ans = 3
):
- Process Course 3 (Course 4):
- Remove from queue, decrement
n
to 1
- Remove from queue, decrement
- Process Course 4 (Course 5):
- Remove from queue, decrement
n
to 0
- Remove from queue, decrement
- Queue after Semester 3: empty
Step 4: Check completion
Since n = 0
(all courses completed), return ans = 3
.
Final Schedule:
- Semester 1: Take Courses 1 and 2 (in parallel)
- Semester 2: Take Course 3
- Semester 3: Take Courses 4 and 5 (in parallel)
The algorithm correctly identifies that we need 3 semesters minimum, taking advantage of parallelism where possible while respecting prerequisite constraints.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def minimumSemesters(self, n: int, relations: List[List[int]]) -> int:
6 # Build adjacency list for course dependencies
7 # graph[i] contains list of courses that depend on course i
8 graph = defaultdict(list)
9
10 # Track in-degree (number of prerequisites) for each course
11 in_degree = [0] * n
12
13 # Process all prerequisite relations
14 for prerequisite, next_course in relations:
15 # Convert to 0-indexed (courses are 1-indexed in input)
16 prerequisite -= 1
17 next_course -= 1
18
19 # Add edge from prerequisite to next course
20 graph[prerequisite].append(next_course)
21
22 # Increment in-degree for the dependent course
23 in_degree[next_course] += 1
24
25 # Initialize queue with all courses that have no prerequisites
26 queue = deque()
27 for course_id, prerequisite_count in enumerate(in_degree):
28 if prerequisite_count == 0:
29 queue.append(course_id)
30
31 # Track number of semesters needed
32 semesters = 0
33
34 # Process courses level by level (BFS)
35 while queue:
36 # Increment semester count for this batch of courses
37 semesters += 1
38
39 # Process all courses that can be taken this semester
40 courses_this_semester = len(queue)
41 for _ in range(courses_this_semester):
42 current_course = queue.popleft()
43
44 # Decrement total courses remaining
45 n -= 1
46
47 # Update prerequisites for dependent courses
48 for dependent_course in graph[current_course]:
49 in_degree[dependent_course] -= 1
50
51 # If all prerequisites met, add to queue for next semester
52 if in_degree[dependent_course] == 0:
53 queue.append(dependent_course)
54
55 # If all courses taken, return semesters; otherwise, cycle exists
56 return semesters if n == 0 else -1
57
1class Solution {
2 public int minimumSemesters(int n, int[][] relations) {
3 // Create adjacency list to represent the graph
4 List<Integer>[] graph = new List[n];
5 Arrays.setAll(graph, index -> new ArrayList<>());
6
7 // Track in-degree (number of prerequisites) for each course
8 int[] inDegree = new int[n];
9
10 // Build the graph and calculate in-degrees
11 for (int[] relation : relations) {
12 int prerequisite = relation[0] - 1; // Convert to 0-indexed
13 int nextCourse = relation[1] - 1; // Convert to 0-indexed
14 graph[prerequisite].add(nextCourse);
15 inDegree[nextCourse]++;
16 }
17
18 // Queue for BFS - start with courses that have no prerequisites
19 Deque<Integer> queue = new ArrayDeque<>();
20 for (int course = 0; course < n; course++) {
21 if (inDegree[course] == 0) {
22 queue.offer(course);
23 }
24 }
25
26 // Process courses level by level (semester by semester)
27 int semesters = 0;
28 int coursesRemaining = n;
29
30 while (!queue.isEmpty()) {
31 semesters++;
32 int coursesThisSemester = queue.size();
33
34 // Process all courses that can be taken this semester
35 for (int i = 0; i < coursesThisSemester; i++) {
36 int currentCourse = queue.poll();
37 coursesRemaining--;
38
39 // Update prerequisites for dependent courses
40 for (int dependentCourse : graph[currentCourse]) {
41 inDegree[dependentCourse]--;
42 // If all prerequisites met, can take in next semester
43 if (inDegree[dependentCourse] == 0) {
44 queue.offer(dependentCourse);
45 }
46 }
47 }
48 }
49
50 // If all courses completed, return semesters; otherwise, cycle exists
51 return coursesRemaining == 0 ? semesters : -1;
52 }
53}
54
1class Solution {
2public:
3 int minimumSemesters(int n, vector<vector<int>>& relations) {
4 // Build adjacency list graph and track in-degrees
5 vector<vector<int>> adjacencyList(n);
6 vector<int> inDegree(n);
7
8 // Process each prerequisite relation
9 for (auto& relation : relations) {
10 int prerequisite = relation[0] - 1; // Convert to 0-indexed
11 int course = relation[1] - 1; // Convert to 0-indexed
12
13 adjacencyList[prerequisite].push_back(course);
14 ++inDegree[course];
15 }
16
17 // Initialize queue with all courses that have no prerequisites
18 queue<int> courseQueue;
19 for (int i = 0; i < n; ++i) {
20 if (inDegree[i] == 0) {
21 courseQueue.push(i);
22 }
23 }
24
25 // BFS to process courses level by level (semester by semester)
26 int semesters = 0;
27 int remainingCourses = n;
28
29 while (!courseQueue.empty()) {
30 ++semesters;
31 int coursesThisSemester = courseQueue.size();
32
33 // Process all courses that can be taken this semester
34 for (int i = 0; i < coursesThisSemester; ++i) {
35 int currentCourse = courseQueue.front();
36 courseQueue.pop();
37 --remainingCourses;
38
39 // Update prerequisites for dependent courses
40 for (int& nextCourse : adjacencyList[currentCourse]) {
41 --inDegree[nextCourse];
42
43 // If all prerequisites met, course can be taken next semester
44 if (inDegree[nextCourse] == 0) {
45 courseQueue.push(nextCourse);
46 }
47 }
48 }
49 }
50
51 // If all courses completed, return semesters; otherwise cycle exists
52 return remainingCourses == 0 ? semesters : -1;
53 }
54};
55
1/**
2 * Calculates the minimum number of semesters needed to take all courses
3 * @param n - The total number of courses (labeled from 1 to n)
4 * @param relations - Array of [prerequisite, course] pairs indicating dependencies
5 * @returns The minimum number of semesters needed, or -1 if impossible
6 */
7function minimumSemesters(n: number, relations: number[][]): number {
8 // Build adjacency list for the course dependency graph
9 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
10
11 // Track the in-degree (number of prerequisites) for each course
12 const inDegree: number[] = new Array(n).fill(0);
13
14 // Process all course relations to build the graph
15 for (const [prerequisite, course] of relations) {
16 // Convert to 0-indexed and add edge from prerequisite to course
17 adjacencyList[prerequisite - 1].push(course - 1);
18 // Increment in-degree for the dependent course
19 inDegree[course - 1]++;
20 }
21
22 // Initialize queue with all courses that have no prerequisites
23 const queue: number[] = [];
24 for (let courseIndex = 0; courseIndex < n; courseIndex++) {
25 if (inDegree[courseIndex] === 0) {
26 queue.push(courseIndex);
27 }
28 }
29
30 // Track the number of semesters needed
31 let semesterCount: number = 0;
32
33 // Process courses level by level (BFS)
34 while (queue.length > 0) {
35 // Each iteration represents one semester
36 semesterCount++;
37
38 // Process all courses available in current semester
39 const currentSemesterSize: number = queue.length;
40 for (let i = 0; i < currentSemesterSize; i++) {
41 // Take a course from the current semester
42 const currentCourse: number = queue.shift()!;
43 // Decrement remaining courses count
44 n--;
45
46 // Check all courses that depend on the current course
47 for (const nextCourse of adjacencyList[currentCourse]) {
48 // Reduce in-degree as prerequisite is completed
49 inDegree[nextCourse]--;
50 // If all prerequisites are met, course can be taken next semester
51 if (inDegree[nextCourse] === 0) {
52 queue.push(nextCourse);
53 }
54 }
55 }
56 }
57
58 // If all courses are taken (n becomes 0), return semester count; otherwise, cycle exists
59 return n === 0 ? semesterCount : -1;
60}
61
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm performs a topological sort using BFS (Kahn's algorithm). Breaking down the operations:
- Building the adjacency list
g
and calculating in-degrees requires iterating through allm
relations:O(m)
- Finding initial nodes with zero in-degree requires scanning all
n
nodes:O(n)
- The BFS traversal visits each node exactly once and processes each edge exactly once when updating in-degrees:
O(n + m)
Therefore, the overall time complexity is O(n + m)
.
Space Complexity: O(n + m)
The space usage consists of:
- The adjacency list
g
stores all edges, requiringO(m)
space - The
indeg
array stores in-degrees for all nodes:O(n)
- The queue
q
can contain at mostn
nodes:O(n)
Therefore, the overall space complexity is O(n + m)
.
Here, n
represents the number of courses and m
represents the number of prerequisite relationships (edges in the directed graph).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Convert Between 1-indexed and 0-indexed
The most common pitfall in this problem is mishandling the indexing conversion. The input courses are numbered from 1 to n (1-indexed), but arrays in most programming languages are 0-indexed.
Incorrect Implementation:
# WRONG: Using 1-indexed directly with 0-indexed arrays indeg = [0] * n for prev, nxt in relations: g[prev].append(nxt) # This will cause index out of bounds when prev or nxt equals n indeg[nxt] += 1 # Array indices go from 0 to n-1, not 1 to n
Correct Implementation:
# CORRECT: Convert to 0-indexed before using as array indices indeg = [0] * n for prev, nxt in relations: prev, nxt = prev - 1, nxt - 1 # Convert from 1-indexed to 0-indexed g[prev].append(nxt) indeg[nxt] += 1
2. Not Processing All Courses in Current Semester Before Moving to Next
Another pitfall is incorrectly counting semesters by not properly batching courses that can be taken simultaneously.
Incorrect Implementation:
# WRONG: This counts each course as a separate semester ans = 0 while q: i = q.popleft() ans += 1 # Incrementing for each course instead of each level n -= 1 for j in g[i]: indeg[j] -= 1 if indeg[j] == 0: q.append(j)
Correct Implementation:
# CORRECT: Process all courses available in current semester together
ans = 0
while q:
ans += 1 # Increment once per semester
for _ in range(len(q)): # Process ALL courses available this semester
i = q.popleft()
n -= 1
for j in g[i]:
indeg[j] -= 1
if indeg[j] == 0:
q.append(j)
3. Using Wrong Data Structure for Queue Operations
Using a regular list instead of deque can lead to inefficient operations.
Inefficient Implementation:
# INEFFICIENT: Using list with pop(0) is O(n) operation
q = [i for i, v in enumerate(indeg) if v == 0]
while q:
i = q.pop(0) # O(n) operation - shifts all remaining elements
Efficient Implementation:
# EFFICIENT: Using deque for O(1) operations
from collections import deque
q = deque(i for i, v in enumerate(indeg) if v == 0)
while q:
i = q.popleft() # O(1) operation
These pitfalls can cause incorrect results, runtime errors, or inefficient performance. Always validate index conversions, ensure proper level-wise processing for BFS, and use appropriate data structures for optimal performance.
How many times is a tree node visited in a depth first search?
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!