2895. Minimum Processing Time
Problem Description
In this problem, we are managing a set of tasks and processors in a way that optimizes the total time required to complete all tasks. We have n
processors, each with 4
cores, meaning each processor can work on up to 4 tasks simultaneously. We also have n * 4
tasks that need to be completed, with the stipulation that each core can only execute one task at a time.
We are provided with two arrays: processorTime
and tasks
. The processorTime
array tells us when each processor will become available to start executing tasks, with the index of the array representing each processor. The tasks
array indicates the time each individual task takes to execute.
The objective is to determine the minimum amount of time needed to finish all n * 4
tasks when they are distributed across the n
processors and their cores. It's a scheduling problem where we must figure out an optimal assignment of tasks to processors to minimize the overall completion time.
Intuition
To develop the solution approach, consider if we had all processors available at the start, we would likely start with the longest tasks to get them out of the way while all processors are available. We would then fill in the shorter tasks where possible.
However, since processors become available at different times, we want to pair the longest tasks with the soonest available processors. This way, we can ensure that a task doesn't get unnecessarily delayed waiting for a busy processor when it could be started earlier by a free processor.
Here is how we arrive at our solution approach:
- Sort the
processorTime
array in ascending order, so we know the order in which processors become available. - Sort the
tasks
array in descending order, so we have the longest tasks at the beginning of the list.
Once we have these sorted lists, we begin assigning tasks to processors. Starting with the first processor in processorTime
(the one that becomes available earliest), we'll assign it the current longest task available in tasks
. Since each processor has 4 cores, we can assign up to 4 tasks at a time to each processor. So, for every processor, we will assign up to 4 of our current longest tasks, then move to the next processor that becomes available.
To compute the minimum time all tasks are completed, we track the end time for each processor assigned tasks by adding the longest task time to the processor's available time. The minimum completion time is the maximum of these end times since all tasks need to be finished.
By implementing this greedy algorithm, we ensure that we're always using the earliest available processor time optimally by pairing it with the longest remaining tasks.
Solution Approach
The implementation of the solution follows the Greedy approach combined with sorting, which is a common pattern used in optimization problems. Here's how the provided solution translates our intuition into an algorithm:
-
Sorting - We sort both the
processorTime
andtasks
arrays. The former is sorted in ascending order, so we know which processor will be available the soonest. The latter is sorted in descending order, ensuring we pick the longest tasks first. -
Greedy Assignment - Once sorted, we assign the longest tasks to the earliest available processors - that is, each processor starting from the earliest available will take four of the remaining longest tasks.
-
Calculating Completion Time - For every processor, we calculate the end time which is when the processor becomes available (
processorTime
) plus the time of the longest task assigned to it. This uses themax
function to ensure we are only considering the time at which the process will have completed its longest task, which is indicative of its actual availability after its assigned tasks are done. -
Data Structures - We use basic Python lists to represent the arrays and sort them. No advanced data structures are needed as the problem deals with direct array manipulation.
-
Iteration and Indexing - We iterate over the
processorTime
array, and for each processor, we keep an indexi
that we decrement by 4 (the number of cores per processor) to always select the next set of tasks.
Let's walk through this process with a little more detail with respect to the provided solution code snippet:
- We start by sorting both the
processorTime
and thetasks
arrays with the built-in.sort()
method. - We initialize
ans
, which will keep track of the current maximum completion time of all tasks. - We iterate through the
processorTime
, for each processor, we add its available time to the time of the largest pending task and compare it with the currentans
to update the maximum completion time. - We decrement the index
i
by 4 to move on to the next set of tasks for the following processor.
Note that this approach works since i
is started at the end of the tasks
array, saying that we always add the largest task to the next processor. It's also crucial to note that this calculation gives us the earliest possible end time for each processor to finish one of its four tasks, not all four. This is because once we assign the four longest available tasks to a processor (if available), the processor will not finish all of them at the same time, but the completion time will be bounded by the finish time of the longest one.
Here's the reference solution approach, further elucidated:
class Solution:
def minProcessingTime(self, processorTime: List[int], tasks: List[int]) -> int:
processorTime.sort() # Sort the processorTime array in ascending order
tasks.sort() # Sort the tasks array in descending order (requires reversing since Python's sort is ascending)
ans = 0 # Initialize the answer variable
i = len(tasks) - 1 # Start with the last index of the sorted tasks (the largest task)
for t in processorTime: # Iterate through each processor's available time
ans = max(ans, t + tasks[i]) # Update the answer with the latest finish time for the current processor
i -= 4 # Move the index to the next set of 4 tasks
return ans # Return the final calculated answer
The implementation confirms the intuition that earlier available processors should be assigned the longest tasks in a way that minimizes empty processor time and maximizes task processing overlaps.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach described above:
Assume we have n = 2
processors, and hence n * 4 = 8
tasks. The processors become available at times given by processorTime = [3, 6]
and the individual tasks take times tasks = [5, 2, 3, 7, 8, 1, 4, 6]
.
Following the solution steps:
-
Sorting:
- Sort
processorTime
in ascending order: It's already[3, 6]
. - Sort
tasks
in descending order: It becomes[8, 7, 6, 5, 4, 3, 2, 1]
.
- Sort
-
Greedy Assignment:
- Start with the first processor (
processorTime[0] = 3
) and assign the four longest tasks[8, 7, 6, 5]
. - Move to the second processor (
processorTime[1] = 6
) and assign the remaining four tasks[4, 3, 2, 1]
.
- Start with the first processor (
-
Calculating Completion Time:
- For the first processor, which starts at time
3
, the maximum task time is8
, so it will finish its longest task at time3 + 8 = 11
. - For the second processor, starting at time
6
, the maximum task time is4
, so it will finish at time6 + 4 = 10
.
- For the first processor, which starts at time
-
Determine Final Answer:
- The largest among the completion times for the processors is
11
, which corresponds to the first processor.
- The largest among the completion times for the processors is
Therefore, with this approach, all tasks will be finished in 11
units of time.
The solution code for this example would execute as follows:
class Solution:
def minProcessingTime(self, processorTime: List[int], tasks: List[int]) -> int:
processorTime.sort() # processorTime becomes [3, 6]
tasks.sort(reverse=True) # tasks becomes [8, 7, 6, 5, 4, 3, 2, 1]
ans = 0 # Initialize the answer variable
i = len(tasks) - 1 # i starts at 7, index of the smallest task
for t in processorTime: # Iterate through processor times [3, 6]
ans = max(ans, t + tasks[i]) # ans becomes max(0, 3+5), then max(11, 6+1)
i -= 4 # Decrease i to select the next set of 4 tasks
return ans # Return 11 as the final calculated answer
After running through this example, we have a clear view of how the algorithm assigns tasks to processors in a way that aims to minimize the total completion time.
Solution Implementation
1# Importing List from the typing module for type annotations
2from typing import List
3
4class Solution:
5 def minProcessingTime(self, processor_times: List[int], tasks: List[int]) -> int:
6 # Sort the processor times in ascending order
7 processor_times.sort()
8 # Sort the tasks in ascending order
9 tasks.sort()
10
11 # Initialize the answer for the minimum processing time
12 min_time = 0
13 # Start from the last task, which has the longest processing time
14 task_index = len(tasks) - 1
15
16 # Iterate over each processor to assign the longest task(s) they can process
17 for processor_time in processor_times:
18 # Ensure the task_index is not less than 0 to avoid IndexError
19 if task_index >= 0:
20 # The completion time for the processor is its processing time
21 # added to the task time it will process
22 # We select the max of the current min_time or the new completion time
23 # This represents the earliest time at which all processors have finished processing
24 min_time = max(min_time, processor_time + tasks[task_index])
25 # Decrement the task_index by 4 because the current processor is considered to
26 # process a task every 4 cycles/time units (as per the given decrement step)
27 task_index -= 4
28
29 # Return the minimum time required to process all tasks
30 return min_time
31
1import java.util.Collections;
2import java.util.List;
3
4class Solution {
5
6 /**
7 * Calculates the minimum amount of time required to process all tasks given an array of processor times.
8 *
9 * @param processorTimes A list of integers representing the times each processor requires to be ready for a task.
10 * @param tasks A list of integers representing the times required to process each task.
11 * @return The minimum processing time to complete all tasks.
12 */
13 public int minProcessingTime(List<Integer> processorTimes, List<Integer> tasks) {
14 // Sort the processor times in ascending order
15 Collections.sort(processorTimes);
16
17 // Sort the tasks in ascending order
18 Collections.sort(tasks);
19
20 int minTime = 0; // Variable to store the minimum processing time required
21 int taskIndex = tasks.size() - 1; // Start from the last task (which is the largest due to sorting)
22
23 // Iterate over each processor time
24 for (int processorTime : processorTimes) {
25 // If there are no more tasks to allocate, break the loop
26 if (taskIndex < 0) {
27 break;
28 }
29
30 // Calculate the total time for current processor by adding its ready time to the task time
31 // and update minTime if this is larger than the current minTime
32 minTime = Math.max(minTime, processorTime + tasks.get(taskIndex));
33
34 // Move to the task which is 4 positions earlier in the list since there are 4 processors (0-based index)
35 taskIndex -= 4;
36 }
37
38 // Return the minimum time needed to complete all tasks
39 return minTime;
40 }
41}
42
1#include <vector>
2#include <algorithm> // Include the algorithm header for using the sort function
3
4class Solution {
5public:
6 int minProcessingTime(vector<int>& processorTimes, vector<int>& tasks) {
7 // Sort the processor times in ascending order
8 sort(processorTimes.begin(), processorTimes.end());
9 // Sort the tasks in ascending order
10 sort(tasks.begin(), tasks.end());
11
12 // Initialize the answer to 0. This will track the minimum processing time.
13 int minimumProcessingTime = 0;
14 // Start from the last task and work backwards
15 int taskIndex = tasks.size() - 1;
16
17 // Iterate over processors and assign them the heaviest remaining task
18 for (int processorTime : processorTimes) {
19 if (taskIndex >= 0) { // Check if there are still tasks to process
20 // Update the minimum processing time if it's less than the current processor's time plus task time
21 minimumProcessingTime = max(minimumProcessingTime, processorTime + tasks[taskIndex]);
22 // Move to the next set of tasks, assuming each processor can process 4 tasks simultaneously
23 taskIndex -= 4;
24 } else {
25 // No more tasks to assign, break out of the loop.
26 break;
27 }
28 }
29
30 return minimumProcessingTime; // Return the calculated minimum processing time
31 }
32};
33
1/**
2 * Calculates the minimum time required to process all tasks by assigning them to processors.
3 *
4 * @param {number[]} processorTimes - Array of processor times, where each element represents the time a processor takes to complete a task.
5 * @param {number[]} tasks - Array of task times, where each element represents the time a task requires for processing.
6 * @returns {number} The minimum time required to process all tasks.
7 */
8function minProcessingTime(processorTimes: number[], tasks: number[]): number {
9 // Sort the processor times in ascending order
10 processorTimes.sort((a, b) => a - b);
11 // Sort the tasks in ascending order
12 tasks.sort((a, b) => a - b);
13
14 // Initialize the answer and the index for the tasks array.
15 let answer: number = 0;
16 let taskIndex: number = tasks.length - 1;
17
18 // Loop through each processor time
19 for (const processorTime of processorTimes) {
20 // Calculate the potential time to process the current task with the current processor
21 // and update the answer with the maximum value of the current answer and this potential time
22 answer = Math.max(answer, processorTime + (taskIndex >= 0 ? tasks[taskIndex] : 0));
23 // Decrement the taskIndex by 4 for the next iteration, to simulate assignment of tasks to every 4th processor
24 taskIndex -= 4;
25 }
26
27 // Return the minimum time after all processors have been accounted for
28 return answer;
29}
30
Time and Space Complexity
The time complexity of the code is primarily determined by the sorting operations performed on processorTime
and tasks
which are O(n log n)
where n
is the number of tasks. Each list is sorted exactly once, and therefore the time complexity remains O(n log n)
.
After the sorting, there is a for loop that iterates through processorTime
. The loop itself runs in O(m)
time, where m
is the number of processors. However, this does not affect the overall time complexity since it is assumed that m
is much less than n
and because the m
loop is not nested within an n
loop. Thus, the for loop's complexity does not exceed O(n log n)
of the sorting step.
The space complexity of the sort operation depends on the implementation of the sorting algorithm. Typically, the sort method in Python (Timsort) has a space complexity of O(n)
. However, in the reference answer, they've noted a space complexity of O(log n)
. This can be considered correct under the assumption that the sorting algorithm used is an in-place sort like heapsort or in-place mergesort which has an O(log n)
space complexity due to the recursion stack during the sort, but Python's default sorting algorithm is not in-place and actually takes O(n)
space. If the sizes of the processorTime
and tasks
lists are immutable, and cannot be changed in place, the space complexity could indeed be O(log n)
due to the space used by the sorting algorithm's recursion stack.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!