You are given an integer n
, which represents the number of courses you have to take, labeled from 1
to n
. You are also given an array prerequisites
where prerequisites[i] = [ai, bi]
indicates that you must take course bi
first if you want to take course ai
. You need to find the minimum number of semesters to take all courses. Each semester, you can take any number of courses as long as you have taken all the prerequisites for the course. If it is impossible to take all the courses due to a cycle, return -1
.
For example:
n = 3
and prerequisites = [[1,3],[2,3]]
, this means you need to take courses 1 and 2 before course 3. You can take courses 1 and 2 in the first semester, and course 3 in the second semester, so the answer is 2.n = 3
and prerequisites = [[1,2],[2,3],[3,1]]
, this means you need to take course 2 before course 1, course 3 before course 2, and course 1 before course 3. Since this creates a cycle, it is impossible to finish all courses, so the answer is -1.n = 1
and prerequisites = []
, this means there is only one course and no prerequisites. You can take this course in the first semester, so the answer is 1.Can you provide an algorithm to find the minimum number of semesters required to take all courses, considering the prerequisites? Explain the time and space complexity of your solution. Consider edge cases, such as cyclic dependencies and empty prerequisite lists.
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force approach to figuring out the minimum number of semesters to complete courses involves trying every possible order to take the courses in. We check if each order is valid, meaning no course is taken before its prerequisites. After checking, we select the best valid order based on how many semesters it takes.
Here's how the algorithm would work step-by-step:
import random
def parallel_courses_brute_force(number_of_courses, prerequisites):
best_semesters = float('inf')
for _ in range(1000): # Try many random orderings to find the best
course_order = list(range(1, number_of_courses + 1))
random.shuffle(course_order)
if is_valid_order(course_order, prerequisites, number_of_courses):
#If course order is valid find the number of semesters needed
semesters_needed = calculate_semesters(course_order, prerequisites)
best_semesters = min(best_semesters, semesters_needed)
if best_semesters == float('inf'):
return -1
return best_semesters
def is_valid_order(course_order, prerequisites, number_of_courses):
course_index_map = {course: index for index, course in enumerate(course_order)}
for course, prereqs in enumerate([[] for _ in range(number_of_courses + 1)]):
for pre, post in prerequisites:
if post == course + 1:
prereqs.append(pre)
for post_course in range(1, number_of_courses + 1):
prerequisites_for_course = []
for pre, post in prerequisites:
if post == post_course:
prerequisites_for_course.append(pre)
for pre_course in prerequisites_for_course:
#Check if all prereqs are taken before current course
if course_index_map[pre_course] > course_index_map[post_course]:
return False
return True
def calculate_semesters(course_order, prerequisites):
course_semesters = {}
for course in course_order:
#Assign the earliest semester possible to each course
max_prereq_semester = 0
for pre, post in prerequisites:
if post == course:
max_prereq_semester = max(max_prereq_semester, course_semesters[pre])
course_semesters[course] = max_prereq_semester + 1
return max(course_semesters.values())
The best way to solve this is to think about doing courses in order, taking courses as soon as you can. We need to figure out the fewest number of semesters needed to finish all courses while following the prerequisites.
Here's how the algorithm would work step-by-step:
def parallel_courses(number_of_courses, prerequisites):
indegree = [0] * number_of_courses
adjacency_list = [[] for _ in range(number_of_courses)]
for course, prerequisite in prerequisites:
indegree[course - 1] += 1
adjacency_list[prerequisite - 1].append(course - 1)
# Identify courses with no prerequisites
queue = [i for i in range(number_of_courses) if indegree[i] == 0]
semesters = 0
courses_taken = 0
while queue:
semesters += 1
next_queue = []
# Process all courses that can be taken in parallel
for course in queue:
courses_taken += 1
# Update indegrees of dependent courses
for next_course in adjacency_list[course]:
indegree[next_course] -= 1
if indegree[next_course] == 0:
next_queue.append(next_course)
# Courses for the next semester
queue = next_queue
# Check for cycle if not all courses are taken
if courses_taken != number_of_courses:
return -1
# Total semesters needed to finish all courses
return semesters
Case | How to Handle |
---|---|
Empty input: n = 0, prerequisites = [] | Return an empty list or handle it as an already completed course list, depending on problem requirements. |
Single course with no prerequisites: n = 1, prerequisites = [] | Return a list containing only that single course [0]. |
All courses depend on each other, creating a cycle | Detect cycles in the graph and return an empty list indicating that the courses cannot be finished. |
Maximum number of courses and prerequisites (n is very large) | Ensure the algorithm uses memory efficiently and avoid excessive recursion to prevent stack overflow, potentially using iterative topological sort with a queue. |
A course has no dependencies but other courses depend on it | Add courses with no dependencies to the starting queue for topological sort. |
Duplicate prerequisites for the same course | The data structure storing the prerequisites should treat duplicates as a single dependency. |
Input contains invalid course numbers (e.g., negative numbers or numbers greater than n-1) | Validate course numbers to ensure they are within the valid range and handle or throw an error if invalid values are found. |
Multiple valid course orders exist | The algorithm should return one valid order; the specific order might depend on the implementation of the topological sort. |