You are given an integer n
, which indicates that there are n
courses labeled from 1
to n
. You are also given a 2D integer array relations
where relations[j] = [prevCourse<sub>j</sub>, nextCourse<sub>j</sub>]
denotes that course prevCourse<sub>j</sub>
has to be completed before course nextCourse<sub>j</sub>
(prerequisite relationship). Furthermore, you are given a 0-indexed integer array time
where time[i]
denotes how many months it takes to complete the (i+1)<sup>th</sup>
course.
You must find the minimum number of months needed to complete all the courses following these rules:
Return the minimum number of months needed to complete all the courses.
Note: The test cases are generated such that it is possible to complete every course (i.e., the graph is a directed acyclic graph).
For example:
If n = 3
, relations = [[1,3],[2,3]]
, and time = [3,2,5]
, the output should be 8
. Course 1 takes 3 months, course 2 takes 2 months, and course 3 takes 5 months. Courses 1 and 2 can be started simultaneously. Course 3 can only be started after courses 1 and 2 are complete, which happens at month 3 (due to course 1). Thus the total time is 3 + 5 = 8 months.
As another example, If n = 5
, relations = [[1,5],[2,5],[3,5],[3,4],[4,5]]
, and time = [1,2,3,4,5]
, the output should be 12
. Courses 1, 2, and 3 can be started at month 0 and completed in 1, 2, and 3 months, respectively. Course 4 can only be started after course 3, so it completes at month 7 (3+4). Course 5 can only be started after courses 1, 2, 3, and 4 are complete, so it starts at month 7 and completes at month 12 (7+5).
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 for this problem involves exploring every possible order of taking the courses. We will try every combination of courses we can take at each time step and find the order that minimizes the total time taken.
Here's how the algorithm would work step-by-step:
def parallel_courses_iii_brute_force(number_of_courses, prerequisites, time_to_complete):
minimum_time = float('inf')
def solve(current_course_taken, time_elapsed, completed_courses):
nonlocal minimum_time
# If all courses are completed, update the minimum time.
if len(completed_courses) == number_of_courses:
minimum_time = min(minimum_time, time_elapsed)
return
available_courses = []
for course_index in range(number_of_courses):
if course_index not in completed_courses:
prerequisites_met = True
for prerequisite_course, course in prerequisites:
if course - 1 == course_index and prerequisite_course - 1 not in completed_courses:
prerequisites_met = False
break
if prerequisites_met:
available_courses.append(course_index)
# If there are no available courses, return.
if not available_courses:
return
# Iterate through all available courses and recursively solve for the next course.
for course_to_take in available_courses:
solve(
course_to_take,
time_elapsed + time_to_complete[course_to_take],
completed_courses | {course_to_take},
)
# Determine the initial available courses.
initial_available_courses = []
for course_index in range(number_of_courses):
is_available = True
for prerequisite_course, course in prerequisites:
if course - 1 == course_index:
is_available = False
break
if is_available:
initial_available_courses.append(course_index)
# Start the recursive solving process for each initial available course.
for starting_course in initial_available_courses:
solve(
starting_course,
time_to_complete[starting_course],
{starting_course},
)
# Handle the case where the graph has cycles and no courses can be taken.
if minimum_time == float('inf'):
return -1
return minimum_time
The core idea is to figure out the earliest time we can finish each course, considering its prerequisites and duration. We use a strategy similar to planning a project where some tasks must be done before others, focusing on minimizing the overall completion time.
Here's how the algorithm would work step-by-step:
def parallel_courses_iii(number_of_courses, relations, duration):
incoming_edges = [0] * number_of_courses
outgoing_courses = [[] for _ in range(number_of_courses)]
completion_time = [0] * number_of_courses
for previous_course, next_course in relations:
outgoing_courses[previous_course - 1].append(next_course - 1)
incoming_edges[next_course - 1] += 1
# Courses with no incoming edges can start immediately.
queue = []
for course in range(number_of_courses):
if incoming_edges[course] == 0:
queue.append(course)
completion_time[course] = duration[course]
max_completion_time = 0
while queue:
current_course = queue.pop(0)
max_completion_time = max(max_completion_time, completion_time[current_course])
# Iterate through the courses dependent on this course.
for next_course in outgoing_courses[current_course]:
incoming_edges[next_course] -= 1
# The course can start once its prerequisites are met.
completion_time[next_course] = max(completion_time[next_course],
completion_time[current_course] + duration[next_course])
if incoming_edges[next_course] == 0:
queue.append(next_course)
return max_completion_time
Case | How to Handle |
---|---|
Empty input (n = 0, courses = [], prereqs = []) | Return 0 immediately as there are no courses to take. |
Single course (n = 1, courses = [1], prereqs = []) | Return the duration of the single course courses[0]. |
All courses have zero duration (courses = [0, 0, 0, ...]) | The algorithm should still correctly calculate the minimum time based on dependencies, resulting in 0 if there are no cycles. |
Cyclic dependencies exist in prereqs | Detect cycles using topological sort or DFS and return -1 or an appropriate error value, as a valid schedule is impossible. |
Large number of courses (n approaching maximum allowed value) | Ensure the graph representation and topological sort algorithm scale efficiently (e.g., using adjacency lists instead of matrices). |
All courses are independent (prereqs = []) | Return the maximum value in the duration array. |
Course durations are very large, leading to potential integer overflow in intermediate calculations | Use long data types for storing accumulated times to prevent overflow. |
Course durations are negative, representing some impossible scenario. | Return an error value (e.g., -1) or throw an exception since durations cannot be negative. |