Taro Logo

Parallel Courses III

Hard
Google logo
Google
1 view
Topics:
GraphsDynamic ProgrammingArrays

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:

  1. You may start taking a course at any time if the prerequisites are met.
  2. Any number of courses can be taken at the same time.

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).

Example 1:

n = 3, relations = [[1,3],[2,3]], time = [3,2,5]

Output: 8

Explanation: We start course 1 and course 2 simultaneously at month 0. Course 1 takes 3 months and course 2 takes 2 months to complete respectively. Thus, the earliest time we can start course 3 is at month 3, and the total time required is 3 + 5 = 8 months.

Example 2:

n = 5, relations = [[1,5],[2,5],[3,5],[3,4],[4,5]], time = [1,2,3,4,5]

Output: 12

Explanation: You can start courses 1, 2, and 3 at month 0. You can complete them after 1, 2, and 3 months respectively. Course 4 can be taken only after course 3 is completed, i.e., after 3 months. It is completed after 3 + 4 = 7 months. Course 5 can be taken only after courses 1, 2, 3, and 4 have been completed, i.e., after max(1,2,3,7) = 7 months. Thus, the minimum time needed to complete all the courses is 7 + 5 = 12 months.

What is the most efficient algorithm to solve this problem?

Solution


Naive Approach: Brute Force

One straightforward but inefficient approach is to consider all possible permutations of the courses and, for each permutation, check if it satisfies the prerequisite relationships. If a permutation is valid, calculate the time needed to complete all courses in that order. The minimum time among all valid permutations would be the answer.

This approach is highly inefficient due to the factorial time complexity of generating permutations.

Complexity Analysis

  • Time Complexity: O(n! * m), where n is the number of courses and m is the number of prerequisite relationships (to check if a permutation is valid).
  • Space Complexity: O(n) to store the permutation.

Optimal Approach: Topological Sort with Dynamic Programming

A much more efficient solution involves topological sorting and dynamic programming. Since the graph is a directed acyclic graph (DAG), we can use topological sort to process nodes in a valid order (i.e., all prerequisites of a node are visited before the node itself). During the topological sort, we can use dynamic programming to calculate the earliest finish time for each course.

Here's the algorithm:

  1. Build Adjacency List and In-degree Array: Represent the course dependencies using an adjacency list where adj[i] contains a list of courses that depend on course i. Also, create an in-degree array indegree where indegree[i] represents the number of prerequisites for course i+1.
  2. Initialize DP Array: Create a dp array of size n. dp[i] will store the earliest finish time for course i+1. Initialize dp[i] with time[i] because a course must take at least its own duration to complete.
  3. Topological Sort using a Queue: Add all courses with an in-degree of 0 to a queue. These are the courses that can be started immediately.
  4. Process Courses: While the queue is not empty:
    • Dequeue a course u.
    • For each neighbor v of u (i.e., v depends on u):
      • Update dp[v-1] with max(dp[v-1], dp[u-1] + time[v-1]). This means the earliest finish time for course v is the maximum of its current finish time and the finish time of course u plus the duration of course v.
      • Decrement indegree[v-1].
      • If indegree[v-1] becomes 0, enqueue v.
  5. Find Maximum Finish Time: After the topological sort, the maximum value in the dp array will be the minimum time needed to complete all courses.

Edge Cases

  • Empty relations array: If there are no prerequisites, the answer is simply the maximum value in the time array.
  • Single course: If n is 1, the answer is time[0].

Code Implementation (Python)

from collections import deque

def min_months_to_complete_courses(n, relations, time):
    adj = [[] for _ in range(n + 1)]
    indegree = [0] * n

    for prev, next_ in relations:
        adj[prev].append(next_)
        indegree[next_ - 1] += 1

    dp = [t for t in time]
    queue = deque([i + 1 for i in range(n) if indegree[i] == 0])

    while queue:
        u = queue.popleft()
        for v in adj[u]:
            dp[v - 1] = max(dp[v - 1], dp[u - 1] + time[v - 1])
            indegree[v - 1] -= 1
            if indegree[v - 1] == 0:
                queue.append(v)

    return max(dp)

Complexity Analysis

  • Time Complexity: O(n + m), where n is the number of courses and m is the number of prerequisite relationships. This is because we visit each node and edge once during the topological sort.
  • Space Complexity: O(n + m), to store the adjacency list, in-degree array, dp array, and the queue. In the worst case, the adjacency list could store all possible relations between n courses.