Start learning free

LESSON 454 OF 543 · DYNAMIC PROGRAMMING

Maximum Product Subarray in Rust: O(n) Time, O(1) Space

Practice O(n) Read the full explanation

Given an array of integers nums, find the subarray with the largest product and return that product.

A subarray is a run of one or more elements that stand next to each other in nums. Its product is all of its elements multiplied together.

Example 1:

Input: nums = [2, 3, -2, 4]
Output: 6
Explanation: The subarray [2, 3] has the largest product, 2 × 3 = 6. -2 is the only negative number, so every subarray that includes it has a negative product.

Example 2:

Input: nums = [-2, 0, -1]
Output: 0
Explanation: The answer can't be 2: [-2, -1] is not a subarray, because 0 stands between them. The subarray [0] gives 0, and every other subarray gives 0 or less.

Example 3:

Input: nums = [-2, 3, -4]
Output: 24
Explanation: The whole array: the two negative numbers cancel out, -2 × 3 × -4 = 24.

Constraints:

  • 1 <= nums.len() <= 105
  • -10 <= nums[i] <= 10
  • The product of any subarray of nums fits in a 32-bit integer.

Note:

Your algorithm should run in O(n) time and use O(1) extra space, where n is the length of nums.

STEP 1 OF 9
    Ctrl + Enter to check

    RUST · DYNAMIC PROGRAMMING

    Maximum Product Subarray in Rust, explained

    Problem overview

    max_product receives a vector nums of integers and returns the largest product of a subarray: a run of one or more neighbouring elements, all multiplied together. For [2, 3, -2, 4] the answer is 6, from [2, 3]; the only negative number, -2, makes every subarray that includes it negative. For [-2, 0, -1] the answer is 0: [-2, -1] isn't a subarray, because the 0 stands between them. For [-2, 3, -4] the answer is 24: the two negatives cancel out over the whole array.

    The statement asks for O(n) time and O(1) extra space.

    Approach: track the largest and the smallest product ending here

    The brute force multiplies every subarray, which is O(n²) pairs of ends for a vector of up to 100,000 elements, too slow. A single pass is possible once you notice that the best subarray must end somewhere, so it is enough to know, for each position, the best product of a subarray ending there.

    For a sum, the best subarray ending at element i either starts fresh at i or extends the best one ending at i - 1. A product behaves differently because of signs: a very negative product, multiplied by a negative number, becomes a very positive one.

    So the pass keeps two numbers for the subarrays that end at the current element: the largest product, max_here, and the smallest (most negative) one, min_here. A third variable, result, records the largest max_here seen so far.

    At each new element x, the candidates for a subarray ending there are x alone, max_here * x and min_here * x. When x is negative, multiplying flips the order: the old smallest product gives the new largest. Swapping max_here and min_here before multiplying takes care of that, so afterwards the larger of "x alone" and "max_here * x" is the new maximum, and the smaller of "x alone" and "min_here * x" is the new minimum. Starting fresh at x is also how a 0 resets the run.

    Running it on the examples gives these values at each element:

    [2, 3, -2, 4]:  start max=2 min=2 res=2 | x=3 max=6 min=3 res=6 | x=-2 max=-2 min=-12 res=6 | x=4 max=4 min=-48 res=6
    [-2, 3, -4]:    start max=-2 min=-2 res=-2 | x=3 max=3 min=-6 res=3 | x=-4 max=24 min=-12 res=24
    [-2, 0, -1]:    start max=-2 min=-2 res=-2 | x=0 max=0 min=0 res=0 | x=-1 max=0 min=-1 res=0
    

    Step by step

    1. Create max_here as let mut, starting at nums[0]. At the first element, the only subarray ending there is the element itself.
    2. Create min_here the same way, also nums[0].
    3. Create result the same way. A subarray needs at least one element, so the first element alone is a valid start, even when it is negative.
    4. Loop i over 1..nums.len(). Element 0 is already counted.
    5. Swap when nums[i] is negative, with std::mem::swap(&mut max_here, &mut min_here). After the swap, each variable is the right one to multiply by nums[i].
    6. Update max_here to nums[i].max(max_here * nums[i]): start fresh at i or extend.
    7. Update min_here to nums[i].min(min_here * nums[i]), the same two choices, keeping the smaller product.
    8. Update result to result.max(max_here). The best subarray ends at some element, so the maximum over every max_here is the answer.
    9. Return result with no semicolon.

    The solution in Rust

    impl Solution {
        fn max_product(nums: Vec<i32>) -> i32 {
            let mut max_here = nums[0];
            let mut min_here = nums[0];
            let mut result = nums[0];
            for i in 1..nums.len() {
                if nums[i] < 0 {
                    std::mem::swap(&mut max_here, &mut min_here);
                }
                max_here = nums[i].max(max_here * nums[i]);
                min_here = nums[i].min(min_here * nums[i]);
                result = result.max(max_here);
            }
            result
        }
    }
    

    Rust details worth a closer look:

    • mut. All three variables change inside the loop, so they are declared with let mut. Rust infers i32 from nums[0].
    • std::mem::swap. It takes two mutable borrows, &mut max_here and &mut min_here, and exchanges the values behind them. It avoids a temporary variable.
    • max and min as methods. nums[i].max(max_here * nums[i]) calls max on an i32 that was read from the vector; because i32 is Copy, reading nums[i] doesn't move anything.
    • Range. 1..nums.len() excludes the end, so i is a valid index. For a one-element vector the range is empty, and the function returns nums[0].
    • Overflow. The lesson promises that every subarray's product fits in 32 bits. max_here and min_here are subarray products too, so the multiplications stay within that. Without that guarantee, an overflow in a build with optimizations would wrap silently, and in a debug build it would panic.

    Complexity

    The loop body does a fixed number of comparisons and multiplications, and nothing grows with the input except the loop count.

    Time: O(n). One pass, with a constant amount of work per element.

    Space: O(1). Three integers, whatever the length of nums.

    Edge cases and common mistakes

    • A single negative element. [-3] gives -3. That is why result starts at nums[0] and not at 0: a subarray must have at least one element.
    • Zeros. [0] gives 0, and [0, 2] gives 2. A 0 makes both running products 0, and the next element starts fresh.
    • Two negatives cancel. [-1, -1] gives 1, and [-2, -3, -4] gives 12, from the last two elements.
    • A zero between negatives. [-2, 0, -1] gives 0, not 2, because the 0 splits the array.
    • Tracking only the maximum. Without min_here, the code can't use a negative product that a later negative number turns into the best one. In [-2, 3, -4] the answer needs the -6 that min_here holds at the second element.
    • Multiplying before swapping. The swap must come before the two updates. Multiplying first would use the wrong value for each.
    • Starting at 0 or 1. It gives wrong answers for all-negative inputs such as [-3].
    • More examples. [2, -5, -2, -4, 3] gives 24, and [1, 0, -1, 2, -3] gives 6.

    Testing

    • [2, 3, -2, 4] gives 6.
    • [-2, 0, -1] gives 0.
    • [-2, 3, -4] gives 24.
    • [-3] gives -3.
    • [0] gives 0.
    • [-2, -3, -4] gives 12.
    • [2, -5, -2, -4, 3] gives 24.
    • [-1, -1] gives 1.
    • [0, 2] gives 2.
    • [1, 0, -1, 2, -3] gives 6.
    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free