2895. Minimum Processing Time
Problem Description
You have a certain number of processors, and each processor has exactly 4 cores. The total number of tasks you need to execute is four times the number of processors (so if you have n
processors, you have 4n
tasks).
Each task must be assigned to a unique core, meaning once a core is used for a task, it cannot be used again. Since each processor has 4 cores and can only be used once, each processor will handle exactly 4 tasks.
You are given two arrays:
processorTime
: An array whereprocessorTime[i]
represents the time when processori
becomes available to start processing taskstasks
: An array wheretasks[j]
represents how long taskj
takes to complete
When a processor becomes available at time t
and is assigned a task that takes duration d
, the task will complete at time t + d
. All 4 cores of a processor start working simultaneously when the processor becomes available.
Your goal is to assign all tasks to processors in a way that minimizes the total time needed to complete all tasks. The total completion time is determined by the last task to finish across all processors.
For example, if you have 2 processors becoming available at times [0, 10]
and 8 tasks with durations [2, 3, 1, 4, 5, 6, 7, 8]
, you need to assign 4 tasks to each processor. The completion time for each processor would be its availability time plus the maximum duration among its assigned tasks.
Return the minimum possible time when all tasks are completed.
Intuition
Think about what determines when all tasks are completed - it's the processor that finishes last. For each processor, its completion time equals its start time plus the duration of its longest task (since all 4 cores work in parallel).
The key insight is that we want to minimize the maximum completion time across all processors. This is a classic min-max optimization problem.
Consider this: if we have a processor that becomes available early, we can afford to give it longer tasks because it has a "head start". Conversely, a processor that becomes available late should get shorter tasks to avoid extending the overall completion time.
This leads us to a greedy pairing strategy: match early processors with long tasks, and late processors with short tasks. More specifically:
- The earliest available processor should handle the 4 longest tasks
- The second earliest processor should handle the next 4 longest tasks
- And so on...
Why does this work? Consider any processor p
with availability time t
. Among the 4 tasks assigned to it, only the longest one matters for determining when this processor finishes (since all 4 cores work in parallel). So we're essentially pairing each processor with the longest task in its group of 4.
By sorting processors in ascending order of availability time and tasks in ascending order of duration, we can systematically assign tasks in groups of 4. Starting from the end of the sorted tasks array (longest tasks), we assign 4 tasks to each processor in order. This ensures that the earliest processors get the longest tasks, balancing out the completion times across all processors.
The final answer is the maximum of processorTime[i] + max_task_duration_for_processor_i
across all processors.
Solution Approach
The implementation follows the greedy strategy we identified: pair early processors with long tasks.
Step 1: Sort both arrays
- Sort
processorTime
in ascending order to get processors from earliest to latest availability - Sort
tasks
in ascending order to arrange tasks from shortest to longest duration
processorTime.sort() tasks.sort()
Step 2: Assign tasks to processors We iterate through each processor and assign it the 4 longest remaining tasks. Since tasks are sorted in ascending order, the longest tasks are at the end of the array.
- Initialize
ans = 0
to track the maximum completion time - Start with index
i = len(tasks) - 1
pointing to the longest task - For each processor with availability time
t
:- The processor will handle tasks at indices
[i, i-1, i-2, i-3]
- Among these 4 tasks, the one at index
i
has the maximum duration - The completion time for this processor is
t + tasks[i]
- Update the overall maximum:
ans = max(ans, t + tasks[i])
- Move the index backward by 4:
i -= 4
- The processor will handle tasks at indices
ans = 0
i = len(tasks) - 1
for t in processorTime:
ans = max(ans, t + tasks[i])
i -= 4
Why we only check tasks[i]
:
Since tasks
is sorted, among the 4 tasks assigned to each processor (indices [i-3, i-2, i-1, i]
), the task at index i
has the maximum duration. All 4 cores work in parallel, so only the longest task determines when this processor finishes.
Time Complexity: O(n log n + m log m)
where n
is the number of processors and m
is the number of tasks. The sorting operations dominate the complexity.
Space Complexity: O(1)
if we can sort in-place, otherwise O(n + m)
for the sorting space.
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 with 2 processors and 8 tasks.
Given:
processorTime = [8, 10]
(processors become available at times 8 and 10)tasks = [2, 2, 3, 1, 8, 7, 4, 5]
(8 tasks with various durations)
Step 1: Sort both arrays
- After sorting
processorTime
:[8, 10]
(already sorted) - After sorting
tasks
:[1, 2, 2, 3, 4, 5, 7, 8]
Step 2: Assign tasks to processors
We start from the end of the sorted tasks array (longest tasks first) and assign 4 tasks to each processor:
First processor (available at time 8):
- Starting index
i = 7
(last element) - Assigns tasks at indices [4, 5, 6, 7] โ tasks with durations [4, 5, 7, 8]
- All 4 cores work in parallel, so completion time = 8 + max(4, 5, 7, 8) = 8 + 8 = 16
- Update
ans = max(0, 16) = 16
- Move index:
i = 7 - 4 = 3
Second processor (available at time 10):
- Current index
i = 3
- Assigns tasks at indices [0, 1, 2, 3] โ tasks with durations [1, 2, 2, 3]
- All 4 cores work in parallel, so completion time = 10 + max(1, 2, 2, 3) = 10 + 3 = 13
- Update
ans = max(16, 13) = 16
Result: The minimum time to complete all tasks is 16.
Why this is optimal:
- We gave the longest tasks [4, 5, 7, 8] to the processor that starts earlier (time 8)
- We gave the shorter tasks [1, 2, 2, 3] to the processor that starts later (time 10)
- This balances the completion times: 16 vs 13
- If we had done the opposite (short tasks to early processor, long tasks to late processor), we'd get completion times of 11 vs 18, with overall time of 18 (worse!)
Solution Implementation
1class Solution:
2 def minProcessingTime(self, processorTime: List[int], tasks: List[int]) -> int:
3 # Sort processors by their available time (ascending order)
4 processorTime.sort()
5
6 # Sort tasks by their execution time (ascending order)
7 tasks.sort()
8
9 # Initialize the maximum completion time
10 max_completion_time = 0
11
12 # Start from the longest task (last element after sorting)
13 task_index = len(tasks) - 1
14
15 # Assign tasks to each processor
16 for processor_start_time in processorTime:
17 # Each processor handles 4 tasks
18 # Assign the longest remaining task to this processor
19 # The completion time for this processor is start time + longest task time
20 max_completion_time = max(max_completion_time,
21 processor_start_time + tasks[task_index])
22
23 # Move to the next batch of 4 tasks for the next processor
24 # We only care about the longest task per processor (at task_index)
25 # since that determines the processor's completion time
26 task_index -= 4
27
28 # Return the minimum possible maximum completion time
29 return max_completion_time
30
1class Solution {
2 public int minProcessingTime(List<Integer> processorTime, List<Integer> tasks) {
3 // Sort processor times in ascending order (fastest processors first)
4 processorTime.sort((a, b) -> a - b);
5
6 // Sort tasks in ascending order (shortest tasks first)
7 tasks.sort((a, b) -> a - b);
8
9 // Initialize the maximum completion time
10 int maxCompletionTime = 0;
11
12 // Start from the longest task (end of sorted list)
13 int taskIndex = tasks.size() - 1;
14
15 // Assign tasks to processors
16 // Strategy: Assign longest tasks to fastest processors
17 for (int currentProcessorTime : processorTime) {
18 // Calculate completion time for current processor
19 // Processor handles the longest remaining task
20 int completionTime = currentProcessorTime + tasks.get(taskIndex);
21
22 // Update maximum completion time across all processors
23 maxCompletionTime = Math.max(maxCompletionTime, completionTime);
24
25 // Move to next set of 4 tasks (each processor handles 4 tasks)
26 taskIndex -= 4;
27 }
28
29 return maxCompletionTime;
30 }
31}
32
1class Solution {
2public:
3 int minProcessingTime(vector<int>& processorTime, vector<int>& tasks) {
4 // Sort processors in ascending order (earliest available first)
5 sort(processorTime.begin(), processorTime.end());
6
7 // Sort tasks in ascending order (shortest tasks first)
8 sort(tasks.begin(), tasks.end());
9
10 // Initialize the maximum completion time
11 int maxCompletionTime = 0;
12
13 // Start from the longest task (last element after sorting)
14 int taskIndex = tasks.size() - 1;
15
16 // Assign tasks to each processor
17 // Strategy: Assign the 4 longest remaining tasks to the earliest available processor
18 for (int currentProcessorTime : processorTime) {
19 // Calculate completion time for this processor
20 // The processor takes the longest task among the 4 assigned to it
21 maxCompletionTime = max(maxCompletionTime, currentProcessorTime + tasks[taskIndex]);
22
23 // Move to the next batch of 4 tasks (skip 3 shorter tasks that run in parallel)
24 taskIndex -= 4;
25 }
26
27 return maxCompletionTime;
28 }
29};
30
1/**
2 * Calculates the minimum time needed to process all tasks using available processors.
3 * Each processor can handle up to 4 tasks simultaneously.
4 * The completion time for a processor is its start time plus the maximum task time assigned to it.
5 *
6 * @param processorTime - Array of available start times for each processor
7 * @param tasks - Array of task execution times to be assigned
8 * @returns The minimum time when all tasks will be completed
9 */
10function minProcessingTime(processorTime: number[], tasks: number[]): number {
11 // Sort processors by their available start time in ascending order
12 processorTime.sort((a: number, b: number) => a - b);
13
14 // Sort tasks by execution time in ascending order
15 tasks.sort((a: number, b: number) => a - b);
16
17 // Initialize the maximum completion time
18 let maxCompletionTime: number = 0;
19
20 // Start from the last (longest) task for greedy assignment
21 let taskIndex: number = tasks.length - 1;
22
23 // Assign tasks to each processor
24 for (const startTime of processorTime) {
25 // Each processor gets the longest remaining task (greedy approach)
26 // This minimizes the overall completion time
27 maxCompletionTime = Math.max(maxCompletionTime, startTime + tasks[taskIndex]);
28
29 // Move to the next batch of 4 tasks for the next processor
30 taskIndex -= 4;
31 }
32
33 return maxCompletionTime;
34}
35
Time and Space Complexity
The time complexity of this code is O(n log n + m log m)
where n
is the number of tasks and m
is the number of processors. Since we have two sorting operations - processorTime.sort()
takes O(m log m)
time and tasks.sort()
takes O(n log n)
time. The for loop iterates through all processors (m
iterations) with constant time operations inside, contributing O(m)
time. Therefore, the overall time complexity is dominated by the sorting operations: O(n log n + m log m)
.
However, given that each processor handles exactly 4 tasks (as seen from i -= 4
), we have the relationship n = 4m
. Substituting this relationship: O(n log n + (n/4) log(n/4)) = O(n log n + n/4 * log n) = O(n log n)
.
The space complexity is O(log n)
which comes from the sorting algorithm's recursive call stack. Python's built-in sort (Timsort) uses O(log n)
space in the worst case for the recursion stack when sorting n
elements.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Task Assignment Direction
A common mistake is assigning the shortest tasks to the earliest processors instead of the longest tasks. This seems intuitive ("start with easy tasks") but actually leads to suboptimal results.
Wrong approach:
# Assigning shortest tasks to earliest processors
task_index = 0
for processor_start_time in processorTime:
max_completion_time = max(max_completion_time,
processor_start_time + tasks[task_index + 3])
task_index += 4
Why it's wrong: This leaves the longest tasks for the latest processors, causing unnecessarily long completion times.
Solution: Always pair early processors with long tasks to maximize parallel processing efficiency.
2. Forgetting to Sort Arrays
Another pitfall is forgetting to sort one or both arrays before assignment, which breaks the greedy strategy entirely.
Wrong approach:
# Missing sort operations
max_completion_time = 0
task_index = len(tasks) - 1
for processor_start_time in processorTime:
max_completion_time = max(max_completion_time,
processor_start_time + tasks[task_index])
task_index -= 4
Solution: Always sort both arrays first - processorTime
in ascending order and tasks
in ascending order before applying the assignment logic.
3. Checking Wrong Task Duration
When assigning 4 tasks to a processor, mistakenly checking a task other than the maximum duration task for that processor.
Wrong approach:
# Checking task at wrong index (not the maximum among the 4)
for processor_start_time in processorTime:
max_completion_time = max(max_completion_time,
processor_start_time + tasks[task_index - 3])
task_index -= 4
Why it's wrong: Since all 4 cores work in parallel, the processor's completion time depends on its longest task, not any arbitrary task.
Solution: Always use tasks[task_index]
(the largest among the 4 assigned tasks) when calculating completion time.
4. Index Out of Bounds
Not handling edge cases or assuming array sizes without validation.
Improved defensive code:
def minProcessingTime(self, processorTime: List[int], tasks: List[int]) -> int:
# Validate input
if not processorTime or not tasks:
return 0
if len(tasks) != 4 * len(processorTime):
raise ValueError("Tasks count must be 4 times processor count")
processorTime.sort()
tasks.sort()
max_completion_time = 0
task_index = len(tasks) - 1
for processor_start_time in processorTime:
if task_index < 0: # Safety check
break
max_completion_time = max(max_completion_time,
processor_start_time + tasks[task_index])
task_index -= 4
return max_completion_time
Which data structure is used to implement priority queue?
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
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!