Find the Minimum Amount of Time to Brew Potions
MedPrompt
You are given two integer arrays, skill and mana, of length n and m, respectively.
In a laboratory, n wizards must brew m potions in order. Each potion has a mana capacity mana[j] and must pass through all the wizards sequentially to be brewed properly. The time taken by the ith wizard on the jth potion is timeij = skill[i] * mana[j].
Since the brewing process is delicate, a potion must be passed to the next wizard immediately after the current wizard completes their work. This means the timing must be synchronized so that each wizard begins working on a potion exactly when it arrives.
Return the minimum amount of time required for the potions to be brewed properly.
Example 1:
Input: skill = [1,5,2,4], mana = [5,1,4,2]
Output: 110
Explanation:
| Potion Number | Start time | Wizard 0 done by | Wizard 1 done by | Wizard 2 done by | Wizard 3 done by |
|---|---|---|---|---|---|
| 0 | 0 | 5 | 30 | 40 | 60 |
| 1 | 52 | 53 | 58 | 60 | 64 |
| 2 | 54 | 58 | 78 | 86 | 102 |
| 3 | 86 | 88 | 98 | 102 | 110 |
As an example for why wizard 0 cannot start working on the 1st potion before time t = 52, consider the case where the wizards started preparing the 1st potion at time t = 50. At time t = 58, wizard 2 is done with the 1st potion, but wizard 3 will still be working on the 0th potion till time t = 60.
Example 2:
Input: skill = [1,1,1], mana = [1,1,1]
Output: 5
Explanation:
- Preparation of the 0th potion begins at time
t = 0, and is completed by timet = 3. - Preparation of the 1st potion begins at time
t = 1, and is completed by timet = 4. - Preparation of the 2nd potion begins at time
t = 2, and is completed by timet = 5.
Example 3:
Input: skill = [1,2,3,4], mana = [1,2]
Output: 21
Constraints:
n == skill.lengthm == mana.length1 <= n, m <= 50001 <= mana[i], skill[i] <= 5000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach is based on dynamic programming principles. We derive a recurrence relation for the optimal start time S_j for each potion j. The key insight is that the entire processing pipeline for a potion might need to be delayed to ensure no wizard is required to work on two potions simultaneously. The optimal delay between potion j-1 and j is determined by finding the most constrained wizard, which requires checking all n wizards. This leads to an O(n) calculation for each of the m potions.
Algorithm
- Let
S_jbe the start time for potionj(when wizard 0 begins). We can assumeS_0 = 0. - The time wizard
ifinishes potionj,finish_time(i, j), can be expressed in terms ofS_jand prefix sums ofskill. LetP_i = skill[0] + ... + skill[i]. Thenfinish_time(i, j) = S_j + mana[j] * P_i. - A wizard
imust be free to start potionj. This implies that the pipeline for potionjmust not overtake the pipeline for potionj-1. This gives the constraintfinish_time(i-1, j) >= finish_time(i, j-1)fori > 0, andstart_time(0, j) >= finish_time(0, j-1)fori=0. - Substituting the formula for
finish_timegives a lower bound forS_jbased onS_{j-1}:S_j >= S_{j-1} + max_{i=0..n-1} (mana[j-1] * P_i - mana[j] * P_{i-1}), whereP_{-1}is taken as 0. - To minimize total time, we choose the earliest possible start time for each potion. We can compute
S_jiteratively:S_j = S_{j-1} + max(0, C_j), whereC_jis themaxterm from the previous step. - First, precompute the prefix sums
Pof theskillarray inO(n)time. - Initialize
current_S = 0(representingS_{j-1}). - Loop
jfrom 1 tom-1. In each iteration: a. CalculateC_jby iteratingifrom 0 ton-1and finding the maximum ofmana[j-1] * P_i - mana[j] * P_{i-1}. This takesO(n)time. b. Updatecurrent_Sby addingmax(0, C_j). - The final result is the finish time of the last wizard on the last potion, which is
S_{m-1} + mana[m-1] * P_{n-1}.
Walkthrough
The core of this method is to find the minimum required start time S_j for each potion j sequentially. We start with S_0 = 0. For each subsequent potion j (from 1 to m-1), we calculate the minimum necessary delay after potion j-1 finishes. This delay is determined by the 'bottleneck' wizard, i.e., the wizard i for whom the constraint finish_time(i-1, j) >= finish_time(i, j-1) imposes the largest delay on S_j relative to S_{j-1}. We iterate through all wizards to find this maximum required delay, update the start time for potion j, and repeat until we have the start time for the last potion. The total time is then easily calculated.
All intermediate products and sums can exceed the capacity of a 32-bit integer, so it's crucial to use 64-bit integers (long in Java) for these calculations to avoid overflow.
class Solution { public long minimumTime(int[] skill, int[] mana) { int n = skill.length; int m = mana.length; // Step 1: Compute prefix sums of skill long[] p = new long[n]; p[0] = skill[0]; for (int i = 1; i < n; i++) { p[i] = p[i-1] + skill[i]; } long currentS = 0; // This will hold S_{j-1} at the start of the loop // Step 2: Iterate through potions to find their start times for (int j = 1; j < m; j++) { long maxTerm = 0; // Find the maximum delay term C_j for (int i = 0; i < n; i++) { long p_i = p[i]; long p_i_minus_1 = (i == 0) ? 0 : p[i-1]; long term = (long)mana[j-1] * p_i - (long)mana[j] * p_i_minus_1; if (term > maxTerm) { maxTerm = term; } } // Update the start time for the current potion j currentS += maxTerm; } // Step 3: Calculate final finish time return currentS + (long)mana[m-1] * p[n-1]; }}Complexity
Time
O(n * m) The algorithm involves pre-calculating prefix sums in `O(n)`. Then, it iterates `m-1` times. Inside this loop, there is another loop that runs `n` times to find the maximum delay term. This results in a total time complexity of `O(n + m*n)`, which simplifies to `O(n*m)`.
Space
O(n) We use an array of size `n` to store the prefix sums of the `skill` array.
Trade-offs
Pros
It's a correct and relatively intuitive approach once the underlying dynamic relationship between potion start times is understood.
Easier to implement compared to more optimized geometric approaches.
Cons
The time complexity of
O(n*m)might be too slow and result in a 'Time Limit Exceeded' error if bothnandmare large (e.g., close to 5000).
This approach optimizes the O(n*m) solution by recognizing that the inner loop's computation is a geometric query that can be solved more efficiently. The calculation of the delay term for each potion, max_{i} (a * P_i - b * P_{i-1}), is equivalent to finding an extreme point on a set of 2D points. By pre-processing these points to build their convex hull, we can answer each query in logarithmic time instead of linear time. This technique is often called the Convex Hull Trick.
Algorithm
- The problem is the same as in the previous approach, but we focus on optimizing the calculation of
C_j = max_{i=0..n-1} (mana[j-1] * P_i - mana[j] * P_{i-1}). - Let
a = mana[j-1]andb = mana[j]. We want to maximizea * P_i - b * P_{i-1}. - Define a set of 2D points
(x_i, y_i) = (P_{i-1}, P_i)fori=0..n-1, whereP_{-1}=0. The problem is now to find a point in this set that maximizesa*y - b*x. - This is a classic computational geometry problem. The point that maximizes such a linear expression must lie on the convex hull of the set of points. Specifically, it will be on the upper convex hull.
- First, compute the prefix sums
Pand generate the points(P_{i-1}, P_i). Since the x-coordinates (P_{i-1}) are sorted, we can build the upper convex hull inO(n)time using a monotone chain algorithm. - For each potion
jfrom 1 tom-1, we have a query. The expressiona*y - b*x = kcorresponds to a liney = (b/a)x + k/a. We want to find the point on the hull that is touched by a line with slopeq = b/athat has the maximum y-intercept. - The slopes of the edges of the upper convex hull are monotonically decreasing. We can use binary search on the hull vertices to find the optimal vertex for the query slope
qinO(log n)time. - The rest of the algorithm proceeds as before, but with the
O(n)inner loop replaced by anO(log n)query on the convex hull.
Walkthrough
We transform the algebraic problem of finding the maximum of a set of linear expressions into a geometric one. The key steps are:
- Point Generation: As in the previous approach, compute prefix sums
Pofskill. Then, createnpoints of the form(P_{i-1}, P_i). Note thatP_{-1}=0. - Convex Hull Construction: Build the upper convex hull of these
npoints. Since the points are already sorted by their x-coordinate (P_{i-1}is non-decreasing), this can be done inO(n)time using a standard algorithm like the monotone chain scan. - Querying: For each potion
jfrom 1 tom-1, we need to find the point(x, y)on the hull that maximizesmana[j-1]*y - mana[j]*x. The functionf(k) = mana[j-1]*y_k - mana[j]*x_kwhere(x_k, y_k)is the k-th vertex of the hull, is bitonic (it increases to a maximum then decreases). We can find this maximum value using binary search on the hull vertices inO(log n)time. - Aggregation: Sum up the delays to find the start time of the last potion and compute the final answer.
import java.util.ArrayList;import java.util.List; class Solution { static class Point { long x, y; Point(long x, long y) { this.x = x; this.y = y; } } // Returns positive if counter-clockwise, negative if clockwise, 0 if collinear private long crossProduct(Point a, Point b, Point c) { return (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x); } public long minimumTime(int[] skill, int[] mana) { int n = skill.length; int m = mana.length; long[] p = new long[n]; p[0] = skill[0]; for (int i = 1; i < n; i++) { p[i] = p[i-1] + skill[i]; } List<Point> points = new ArrayList<>(); points.add(new Point(0, p[0])); // i=0 case: (P_{-1}, P_0) for (int i = 1; i < n; i++) { points.add(new Point(p[i-1], p[i])); } // Build upper convex hull List<Point> hull = new ArrayList<>(); for (Point pt : points) { while (hull.size() >= 2 && crossProduct(hull.get(hull.size() - 2), hull.get(hull.size() - 1), pt) <= 0) { hull.remove(hull.size() - 1); } hull.add(pt); } long currentS = 0; for (int j = 1; j < m; j++) { long a = mana[j-1]; long b = mana[j]; // Binary search on the hull to find the optimal point int low = 0, high = hull.size() - 1; int opt_idx = 0; while (low <= high) { int mid1 = low + (high - low) / 3; int mid2 = high - (high - low) / 3; long val1 = a * hull.get(mid1).y - b * hull.get(mid1).x; long val2 = a * hull.get(mid2).y - b * hull.get(mid2).x; if (val1 < val2) { low = mid1 + 1; } else { high = mid2 - 1; } } opt_idx = low; long max_term = a * hull.get(opt_idx).y - b * hull.get(opt_idx).x; if (max_term > 0) { currentS += max_term; } } return currentS + (long)mana[m-1] * p[n-1]; }}Complexity
Time
O(n + m log n) `O(n)` is needed for computing prefix sums and building the convex hull. Afterwards, for each of the `m-1` potions, we perform a query on the hull. Since the query is done via binary search on the hull vertices (of which there are at most `n`), each query takes `O(log n)` time.
Space
O(n) Space is required for the prefix sums `P`, the list of points, and the vertices of the convex hull. All are proportional to `n`.
Trade-offs
Pros
Highly efficient, with a time complexity that is near-linear in
n+m.It is the optimal solution for the given problem constraints.
Cons
The implementation is significantly more complex, requiring knowledge of computational geometry algorithms like building a convex hull and querying it.
The logic for binary searching on the hull can be tricky to get right.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.