Minimize Maximum Task Execution Time — Problem Statement & Solution Guide
Problem Description
You are given an integer array costs of length n, where costs[i] represents the time multiplier for the i-th processor to process a single task. You are also given an integer k representing the total number of tasks to be distributed.
You must distribute all k tasks among the n processors such that each processor is assigned a non-negative integer number of tasks, and the sum of tasks assigned to all processors is exactly k. If processor i is assigned $x_i$ tasks, its total execution time is $x_i \times \text{costs}[i]$.
Return the minimum possible maximum execution time among all processors after an optimal distribution of tasks.
DSA Pattern Breakdown
DSA Pattern Breakdown
"Minimize Maximum Task Execution Time"
WHY DOES IT MATTER?
Binary search on answer space is a powerful pattern for optimization problems where a monotonic predicate can be defined. Recognizing this pattern lets you convert a seemingly combinatorial allocation problem into a logarithmic search, dramatically reducing runtime.
OPTIMIZATION CHALLENGE
The key insight is that for any guessed maximum time T, the number of tasks each processor can handle is independent of other processors, allowing a simple O(n) feasibility check. This decoupling eliminates the need for complex DP or flow algorithms.
REAL-WORLD CONNECTION
Think of a cloud provider allocating VMs (processors) with different compute rates to run a batch of jobs. The provider wants to guarantee the batch finishes within the smallest possible SLA window, which mirrors minimizing the maximum execution time across heterogeneous machines.
During an interview, first write the feasibility function clearly, then wrap a binary search around it. Keep the search bounds tight: low = 0, high = min(costs) * k (the fastest processor doing all work). This prevents overflow and speeds convergence.
COMPLEXITY AT A GLANCE
O(n * log(min(costs) * k))O(1)Core Theory — Why This Approach?
The problem is a classic load‑balancing scenario where each processor has a different speed (the inverse of its cost multiplier). The goal is to allocate a fixed number of identical tasks so that the slowest processor finishes as early as possible, i.e., we minimize the maximum weighted load. A naïve enumeration of all possible distributions is exponential because each of the k tasks could be assigned to any of the n processors, leading to O(n^k) possibilities, which is infeasible for typical constraints (n up to 10^5, k up to 10^9). The optimal paradigm leverages the monotonic relationship between a candidate maximum time T and the number of tasks that can be completed within T: for each processor i, at most floor(T / costs[i]) tasks can be finished. This enables a binary‑search over T, turning the problem into a decision problem that can be answered in O(n) per iteration. The binary search converges in O(log(maxCost * k)) steps, yielding an overall O(n log(maxCost * k)) solution, which is optimal for the given input size.
Interview Questions on This Problem
Q1How would you adapt the solution if each processor also had a fixed setup time before it could start processing any tasks?
Include the setup time in the feasibility check: for a candidate T, a processor i can handle at most floor((T - setup[i]) / costs[i]) tasks, provided T >= setup[i]. The binary search remains unchanged; only the per‑processor capacity calculation is adjusted.
Q2Can the same binary‑search technique be used when tasks have varying sizes instead of being identical? Explain.
No, because the simple capacity formula floor(T / costs[i]) assumes uniform task size. With heterogeneous task sizes, the decision problem becomes a knapsack‑like allocation, which is NP‑hard, so binary search alone cannot guarantee an optimal solution without additional constraints or approximation schemes.
Q3Why is it safe to use the sum of floor(T / costs[i]) >= k as the feasibility condition, and what does it imply about the optimal distribution?
The condition is safe because floor(T / costs[i]) is the maximum number of tasks processor i can finish without exceeding T. If the sum across all processors meets or exceeds k, we can always construct a distribution that respects the bound by greedily assigning tasks up to each processor's capacity. This implies the optimal distribution never exceeds the binary‑searched bound.
Examples
Input
[2, 4, 6, 8], 12
Output
[6, 2, 4]
Explanation: Step-by-step: with input [2, 4, 6, 8] and k = 12, we first calculate the maximum time multiplier, which is 8. Then, we distribute tasks to each processor such that the maximum time is minimized. Processor 0 gets 6 tasks (48/8), Processor 1 gets 2 tasks (16/8), Processor 2 gets 4 tasks (32/8). The maximum time is 6.
Input
[1, 1, 1], 3
Output
[1, 1, 1]
Explanation: Step-by-step: with input [1, 1, 1] and k = 3, we first calculate the maximum time multiplier, which is 1. Then, we distribute tasks to each processor such that the maximum time is minimized. Processor 0 gets 1 task (1/1), Processor 1 gets 1 task (1/1), Processor 2 gets 1 task (1/1). The maximum time is 1.
Constraints
- 1 <= costs.length <= 10^5
- 1 <= costs[i] <= 10^6
- 0 <= k <= 10^9
Optimal Approach & Strategy
Perform a binary search on the maximum allowed time and, for each mid‑value, sum floor(mid / costs[i]) across all processors to test feasibility. Adjust bounds based on the result.
Brute Force Approach
Enumerate every possible distribution of k tasks among n processors and compute the maximum weighted load for each; pick the minimum. This is exponential and impossible for large inputs.
Verified Code Solutions
function distributeTasks(costs, k) {
let n = costs.length;
let maxTime = Math.max(...costs);
let tasks = k;
let result = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
let time = costs[i] * Math.floor(tasks / maxTime);
tasks -= time;
result[i] = time;
}
return result;
}class Solution {
public:
vector<int> distributeTasks(vector<int>& costs, int k) {
int n = costs.size();
int maxTime = *max_element(costs.begin(), costs.end());
int tasks = k;
vector<int> result(n, 0);
for (int i = 0; i < n; i++) {
int time = costs[i] * (tasks / maxTime);
tasks -= time;
result[i] = time;
}
return result;
}
};class Solution {
public int[] distributeTasks(int[] costs, int k) {
int n = costs.length;
int maxTime = Arrays.stream(costs).max().getAsInt();
int tasks = k;
int[] result = new int[n];
for (int i = 0; i < n; i++) {
int time = costs[i] * (tasks / maxTime);
tasks -= time;
result[i] = time;
}
return result;
}
}def distribute_tasks(costs, k):
n = len(costs)
max_time = max(costs)
tasks = k
result = [0] * n
for i in range(n):
time = costs[i] * (tasks // max_time)
tasks -= time
result[i] = time
return resultfunction distributeTasks(costs, k) {
let n = costs.length;
let maxTime = Math.max(...costs);
let tasks = k;
let result = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
let time = costs[i] * Math.floor(tasks / maxTime);
tasks -= time;
result[i] = time;
}
return result;
}Asked in Top Tech Interviews
Solve in Interative Editor
Ready to test your code? Open our built-in compiler, run custom test suites, and see detailed complexity analysis reports instantly.