Facebook Pixel

3494. Find the Minimum Amount of Time to Brew Potions

MediumArrayPrefix SumSimulation
LeetCode ↗

Problem Description

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 j has a mana capacity mana[j] and must pass through all the wizards sequentially to be brewed properly. The time taken by the i-th wizard on the j-th potion is time[i][j] = skill[i] * mana[j].

The brewing process is delicate, so a potion must be passed to the next wizard immediately after the current wizard finishes their work. In other words, the timing has to be synchronized: each wizard begins working on a potion exactly at the moment it arrives, with no idle gap between a wizard completing one potion and the next wizard picking it up. To make this synchronization possible, the start of a potion's brewing on the first wizard may be delayed so that the whole chain of wizards processes it without any waiting in between.

Your task is to return the minimum amount of time required for all m potions to be brewed properly, given that the potions are brewed in the given order and each potion flows through wizard 0, wizard 1, …, wizard n - 1 in turn.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Simulation / Basic DSA?

This problem maps to Simulation / Basic DSA through a short path in the full flowchart.

SimulationorstraightforwardyesMath orbittricks?noSimulation /Basic DSA

The problem simulates a pipeline processing schedule where each potion flows through wizards in order without gaps, requiring a forward-backward pass.

Open in Flowchart

Intuition

The key constraint is that once a potion starts on the first wizard, it must flow through all the wizards without any pause in between. That is, wizard i + 1 must begin a potion at the exact moment wizard i finishes it. This means the entire journey of a single potion through the wizards is a rigid, "gap-free" chain: if we know when the potion finishes on the last wizard, we can compute when it starts and finishes on every other wizard by subtracting the processing times.

Now think about two consecutive potions. Each wizard processes the potions in order, so wizard i can only begin the next potion after he has finished the current one. At the same time, the next potion's chain is also rigid. The only flexibility we have is when to release the next potion onto the first wizard. We want to release it as early as possible, but not so early that any wizard would have to start working on it before finishing the previous potion (which is impossible, since a wizard can only do one thing at a time).

So for the current potion, let f[i] record the time when wizard i finished the previous potion. We sweep through the wizards from first to last, tracking tot, the time the current potion finishes at the wizard we're looking at. For wizard i, he cannot start the current potion before two things are both true:

  • the current potion has actually reached him (that's tot, the finishing time at the previous wizard), and
  • he has finished the previous potion (that's f[i]).

Therefore his start time is max(tot, f[i]), and his finish time is max(tot, f[i]) + skill[i] * mana[x]. Taking the max automatically pushes the whole chain forward by just enough to avoid any conflict, which gives the earliest valid completion time for the current potion.

After computing tot for the last wizard, that value is the finishing time of the current potion at wizard n - 1. Because the chain is gap-free, we can recover the finishing time of the current potion at every earlier wizard by walking backwards: wizard i finishes exactly when wizard i + 1 starts, so f[i] = f[i + 1] - skill[i + 1] * mana[x]. These updated f values become the "previous potion" finishing times for the next potion, and we repeat.

After processing all potions, f[n - 1] holds the finishing time of the last potion on the last wizard, which is exactly the minimum total brewing time.

Pattern Learn more about Prefix Sum patterns.

Solution Approach

Solution 1: Dynamic Programming

We define f[i] as the time when wizard i completes the previous potion. This single array carries all the state we need from one potion to the next.

We process the potions one by one in order. For the current potion with mana value x:

  1. Initialize tot = 0. Here tot represents the completion time of the current potion at the wizard we are currently looking at.

  2. Sweep forward through the wizards i from 0 to n - 1. Wizard i can only start the current potion when both the potion has reached him (tot) and he has finished the previous potion (f[i]), so his start time is max(tot, f[i]). The processing time is skill[i] * x, giving a completion time of:

    tot = max(tot, f[i]) + skill[i] * x

    After the loop, tot is the time the current potion finishes on the last wizard.

  3. Update the completion times so they describe the current potion (which becomes the "previous" potion for the next round). For the last wizard, set f[n - 1] = tot directly. For the other wizards, use the gap-free property: wizard i finishes exactly when wizard i + 1 starts, so traversing in reverse:

    f[i] = f[i + 1] - skill[i + 1] * x

After all potions have been processed, f[n - 1] holds the finishing time of the last potion on the last wizard, which is the answer.

In the code, a custom max = lambda a, b: a if a > b else b is defined to speed up the inner-loop comparison compared to Python's built-in max. The arrays and variables used are:

  • f: a length-n list storing each wizard's previous-potion completion time.
  • tot: a running value tracking the current potion's completion time as it flows through the wizards.

The outer loop runs over the m potions and the inner loops run over the n wizards, so the time complexity is O(m * n). The space complexity is O(n) for the f array.

Example Walkthrough

Let's trace through a small example to see how the solution approach works.

Input:

  • skill = [1, 2, 3] (so n = 3 wizards)
  • mana = [4, 2] (so m = 2 potions)

The processing time for wizard i on potion j is skill[i] * mana[j].

We maintain f, where f[i] is the time wizard i finished the previous potion. Initially f = [0, 0, 0].


Potion 0 (mana x = 4):

We sweep forward, tracking tot (current potion's completion time at the wizard we're looking at), starting with tot = 0.

Wizard if[i]start = max(tot, f[i])skill[i] * xtot = start + skill[i]*x
00max(0, 0) = 01 * 4 = 40 + 4 = 4
10max(4, 0) = 42 * 4 = 84 + 8 = 12
20max(12, 0) = 123 * 4 = 1212 + 12 = 24

So potion 0 finishes on the last wizard at time tot = 24.

Now update f to describe potion 0, walking backwards using the gap-free property (f[i] = f[i+1] - skill[i+1] * x):

  • f[2] = tot = 24
  • f[1] = f[2] - skill[2] * x = 24 - 3*4 = 12
  • f[0] = f[1] - skill[1] * x = 12 - 2*4 = 4

Now f = [4, 12, 24].

This makes sense: potion 0 occupies wizard 0 during [0, 4], wizard 1 during [4, 12], wizard 2 during [12, 24] — a perfectly gap-free chain.


Potion 1 (mana x = 2):

Reset tot = 0 and sweep forward, now respecting that each wizard must first finish potion 0 (f values).

Wizard if[i]start = max(tot, f[i])skill[i] * xtot = start + skill[i]*x
04max(0, 4) = 41 * 2 = 24 + 2 = 6
112max(6, 12) = 122 * 2 = 412 + 4 = 16
224max(16, 24) = 243 * 2 = 624 + 6 = 30

Notice the max doing its job: at wizard 1, potion 1 reached him at time 6, but he was still busy with potion 0 until 12, so he starts at 12. At wizard 2, potion 1 would arrive at 16, but he's busy until 24, so he starts at 24.

Potion 1 finishes on the last wizard at tot = 30.

Update f backwards:

  • f[2] = 30
  • f[1] = f[2] - skill[2] * x = 30 - 3*2 = 24
  • f[0] = f[1] - skill[1] * x = 24 - 2*2 = 20

Now f = [20, 24, 30].


Result:

All potions processed. The answer is f[n - 1] = f[2] = 30.

Verification of the gap-free constraint for potion 1: wizard 0 works [18, 20]... wait — actually the chain is wizard 0 [20-2, 20] = [18, 20]? Let's confirm via the recovered finish times: wizard 0 finishes at 20, wizard 1 starts at 20 and finishes at 24, wizard 2 starts at 24 and finishes at 30. So potion 1 flows wizard0 [18,20] → wizard1 [20,24] → wizard2 [24,30] with no gaps. Its start on the first wizard was delayed to 18 (rather than starting right at 6) precisely so the whole chain stays synchronized while never overlapping potion 0 on any wizard. This is exactly the synchronization the problem demands, and 30 is the minimum total brewing time.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def minTime(self, skill: List[int], mana: List[int]) -> int:
6        # Number of wizards
7        n = len(skill)
8
9        # finish_time[i] holds the time at which wizard i finishes
10        # processing the current potion
11        finish_time = [0] * n
12
13        # Process each potion in order
14        for current_mana in mana:
15            # Compute the finish time of the last wizard for this potion.
16            # The potion can only move to wizard i after both:
17            #   1. wizard i has finished the previous potion (finish_time[i]), and
18            #   2. the potion has finished at wizard i-1 (accumulated in running_time).
19            # Each wizard i takes skill[i] * current_mana time units.
20            running_time = 0
21            for i in range(n):
22                running_time = max(running_time, finish_time[i]) + skill[i] * current_mana
23
24            # running_time is now the finish time of the last wizard
25            finish_time[-1] = running_time
26
27            # Walk backwards to recover the finish time of each earlier wizard.
28            # Because the potion flows continuously without gaps once it starts,
29            # wizard i finishes exactly skill[i+1] * current_mana before wizard i+1.
30            for i in range(n - 2, -1, -1):
31                finish_time[i] = finish_time[i + 1] - skill[i + 1] * current_mana
32
33        # The time the last wizard finishes the last potion is the answer
34        return finish_time[-1]
35
1class Solution {
2    /**
3     * Computes the minimum total time for all potions to be processed through
4     * a sequence of wizards (a flow-shop scheduling problem).
5     *
6     * Each potion j must pass through every wizard i in order. The time wizard i
7     * spends on potion j is skill[i] * mana[j]. A wizard can only start a potion
8     * after finishing the previous potion AND after the previous wizard has
9     * finished that same potion (no waiting allowed between wizards once started).
10     *
11     * @param skill the processing factor of each wizard, in order
12     * @param mana  the size of each potion, in processing order
13     * @return the finish time of the last potion at the last wizard
14     */
15    public long minTime(int[] skill, int[] mana) {
16        int wizardCount = skill.length;
17
18        // finishTime[i] holds the time at which wizard i finishes the
19        // potion currently being tracked.
20        long[] finishTime = new long[wizardCount];
21
22        // Process each potion in the given order.
23        for (int potion : mana) {
24            long currentTime = 0;
25
26            // Forward pass: compute the finish time at each wizard for this potion.
27            // A wizard can only begin after both:
28            //   (1) it has finished its previous potion (finishTime[i]), and
29            //   (2) the running cumulative time so far (currentTime).
30            for (int i = 0; i < wizardCount; ++i) {
31                currentTime = Math.max(currentTime, finishTime[i]) + (long) skill[i] * potion;
32            }
33
34            // The last wizard's finish time for this potion is fixed.
35            finishTime[wizardCount - 1] = currentTime;
36
37            // Backward pass: since the potion flows continuously without gaps,
38            // back-calculate each earlier wizard's finish time by subtracting
39            // the processing time of the next wizard for this potion.
40            for (int i = wizardCount - 2; i >= 0; --i) {
41                finishTime[i] = finishTime[i + 1] - (long) skill[i + 1] * potion;
42            }
43        }
44
45        // The answer is when the last wizard finishes the last potion.
46        return finishTime[wizardCount - 1];
47    }
48}
49
1class Solution {
2public:
3    long long minTime(vector<int>& skill, vector<int>& mana) {
4        int numWizards = skill.size();
5
6        // finishTime[i] records the time at which potion currently being
7        // processed leaves wizard i (i.e., finishes at wizard i).
8        vector<long long> finishTime(numWizards);
9
10        // Process each potion in order; potions cannot overtake one another,
11        // so they form a strict pipeline through the wizards.
12        for (int currentMana : mana) {
13            long long runningTime = 0;
14
15            // Forward pass: push the current potion through all wizards.
16            // At each wizard the potion can only start once both:
17            //   1) the previous potion has left this wizard (finishTime[i]), and
18            //   2) this potion has finished at the previous wizard (runningTime).
19            // We then add the brewing cost skill[i] * currentMana.
20            for (int i = 0; i < numWizards; ++i) {
21                runningTime = max(runningTime, finishTime[i]) + 1LL * skill[i] * currentMana;
22            }
23
24            // The last wizard's finish time is the running total.
25            finishTime[numWizards - 1] = runningTime;
26
27            // Backward pass: reconstruct each wizard's finish time for this
28            // potion by subtracting the brewing cost of the next wizard.
29            // Since the potion flows continuously without gaps once started,
30            // finishTime[i] = finishTime[i + 1] - cost incurred at wizard (i + 1).
31            for (int i = numWizards - 2; i >= 0; --i) {
32                finishTime[i] = finishTime[i + 1] - 1LL * skill[i + 1] * currentMana;
33            }
34        }
35
36        // After all potions are processed, the answer is when the last potion
37        // leaves the final wizard.
38        return finishTime[numWizards - 1];
39    }
40};
41
1/**
2 * Compute the minimum total time to process all potions through all labs,
3 * where each potion must pass through labs in order without gaps,
4 * and each lab processes potions in the given order.
5 *
6 * @param skill - Time multipliers for each lab (length n).
7 * @param mana  - Mana values for each potion (length m).
8 * @returns The earliest time the last potion finishes at the last lab.
9 */
10function minTime(skill: number[], mana: number[]): number {
11    // Number of labs.
12    const labCount: number = skill.length;
13
14    // finishTime[i] holds the time at which the current potion
15    // leaves lab i (i.e., finishes processing there).
16    const finishTime: number[] = Array(labCount).fill(0);
17
18    // Process each potion in order.
19    for (const currentMana of mana) {
20        // Forward pass: determine when this potion finishes at the last lab.
21        // At each lab, the potion can only start after the lab is free
22        // (Math.max with the lab's previous finish time), then it spends
23        // skill[i] * currentMana time there.
24        let runningTime: number = 0;
25        for (let i = 0; i < labCount; ++i) {
26            runningTime = Math.max(runningTime, finishTime[i]) + skill[i] * currentMana;
27        }
28
29        // The computed runningTime is the finish time at the last lab.
30        finishTime[labCount - 1] = runningTime;
31
32        // Backward pass: back-fill the finish times for earlier labs so they
33        // align tightly with the last lab (no idle waiting for this potion).
34        // The potion must flow continuously, so lab i finishes exactly
35        // skill[i + 1] * currentMana before lab i + 1.
36        for (let i = labCount - 2; i >= 0; --i) {
37            finishTime[i] = finishTime[i + 1] - skill[i + 1] * currentMana;
38        }
39    }
40
41    // The last lab's finish time after the final potion is the answer.
42    return finishTime[labCount - 1];
43}
44

Time and Space Complexity

  • Time Complexity: O(n × m), where n is the number of wizards (len(skill)) and m is the number of potions (len(mana)).

    The outer loop iterates over each value x in mana, executing m times. For each iteration, there are two inner loops over the n wizards:

    • The first loop for i in range(n) computes the cumulative tot value in O(n) time.
    • The second loop for i in range(n - 2, -1, -1) back-fills the f array in O(n) time.

    Since each mana iteration performs O(n) + O(n) = O(n) work, the total time complexity is O(n × m).

  • Space Complexity: O(n), where n is the number of wizards.

    The only auxiliary data structure that scales with input size is the array f, which has length n. The max lambda and the scalar variables (tot, n, x, i) use constant O(1) space. Therefore, the overall space complexity is O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Treating the brewing as a simple pipeline and forgetting the "no idle gap" (synchronization) constraint

The most common mistake is to model this problem like a standard assembly line where each wizard simply picks up a potion as soon as it arrives and he is free, computing only a forward pass:

# WRONG: naive forward-only pipeline
running_time = 0
for i in range(n):
    running_time = max(running_time, finish_time[i]) + skill[i] * current_mana
    finish_time[i] = running_time   # <-- bug: ignores the gap-free constraint

This forward-only update lets a wizard sit idle between finishing one potion and starting the next. But the problem explicitly requires that once a potion starts on wizard 0, it must flow through all wizards with no waiting in between. A wizard cannot hold a finished potion; the downstream wizard must take it the instant it is ready.

Why it's wrong: The naive update assumes wizard i finishes the current potion at the moment computed during the forward sweep. In reality, the start of the potion on wizard 0 may need to be delayed so that the whole chain runs without gaps. The actual finish time of wizard i is anchored by the last wizard's finish time and must be back-propagated.

Solution: After computing the last wizard's true finish time via the forward sweep, walk backwards to recover each earlier wizard's finish time using the gap-free property — wizard i finishes exactly when wizard i + 1 starts:

finish_time[-1] = running_time
for i in range(n - 2, -1, -1):
    finish_time[i] = finish_time[i + 1] - skill[i + 1] * current_mana

This backward pass is the crux of a correct solution and is exactly what the naive pipeline omits.


Pitfall 2: Misinterpreting running_time as a wizard's actual finish time

During the forward sweep, running_time accumulates max(running_time, finish_time[i]) + skill[i] * current_mana. It is tempting to store this intermediate value directly into finish_time[i] for every i. As shown above, this intermediate value is only meaningful for the last wizard. For all earlier wizards it represents a "potion arrives at this point" value, not the synchronized finish time. Only finish_time[-1] should be set from running_time; the rest come from the backward pass.


Pitfall 3: Integer overflow (in other languages) and performance

  • Overflow: In Python this is a non-issue thanks to arbitrary-precision integers. If you port this to C++/Java, skill[i] * mana[j] summed over many wizards and potions can easily exceed 32-bit range, so use long/int64.
  • Performance: The inner loop runs O(m * n) times. Calling Python's built-in max() inside this hot loop is comparatively slow. Defining a local max = lambda a, b: a if a > b else b (or inlining the comparison) avoids the overhead of the variadic built-in and can give a meaningful speedup on large inputs.
max = lambda a, b: a if a > b else b   # faster than built-in for two-arg calls

Pitfall 4: Off-by-one in the backward loop bounds

The backward loop must start at n - 2 (the second-to-last wizard) and go down to 0, because finish_time[n - 1] is already set from running_time. A common slip is starting at n - 1, which would overwrite the just-computed last-wizard value with finish_time[n] - ... (an index error) or corrupt the state. Always anchor the backward pass at index n - 2. Also guard the n == 1 case: with a single wizard the backward loop simply doesn't execute, and finish_time[-1] correctly holds the cumulative sum, so no special-casing is needed.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More