MediumDynamic Programming

Maximum Product Subarray โ€” Solution

Problem

Given an array of integers, find the contiguous subarray that produces the largest product and return that product. Unlike the maximum sum subarray, a negative number can flip a tiny product into a huge one โ€” but only if there's another negative later to flip it back.

  • Input: nums = [2, 3, -2, 4]
  • Output: 6
  • Explanation: The subarray [2, 3] yields the largest product; extending to -2 or 4 makes it smaller.

Counter-example: [-2, 0, -1] returns 0 โ€” the zero breaks any subarray that might span both negatives, so the best we can do is an empty product of 0.

Intuition

The key difference from maximum sum subarray is that negatives can be redeemed: -6 ร— -4 = 24, so a deeply negative running product can suddenly become the largest if we hit another negative. This means tracking only the running maximum is not enough โ€” we must also track the running minimum, because the minimum (most negative) product can flip into the new maximum the moment we multiply by a negative number.

Approach 1 โ€” Brute Force

Try every possible subarray by iterating over all start positions, then extending to each end position and accumulating the product.

  1. Initialize max_product to nums[0] (not 0 โ€” the array can be all negative).
  2. For each starting index, reset the running product to 1.
  3. For each ending index from the start onward, multiply in the current element.
  4. Update max_product if the running product is larger.
  5. Return max_product.
1def maxProduct(nums):
2    max_product = nums[0]
3    for start in range(len(nums)):
4        running_product = 1
5        for end in range(start, len(nums)):
6            running_product *= nums[end]
7            max_product = max(max_product, running_product)
8    return max_product

Time: O(nยฒ) โ€” two nested loops over all subarray boundaries.
Space: O(1) โ€” only a handful of variables.

Approach 2 โ€” Track Max and Min Simultaneously

At each position the best subarray ending here is one of three things: the element alone (start fresh), the element times the previous maximum, or the element times the previous minimum (since negative ร— negative = positive). Track all three and pick the best.

  1. Initialize max_product, current_max, and current_min all to nums[0].
  2. For each subsequent element, compute three candidates: the element alone, element ร— current_max, element ร— current_min.
  3. Capture all candidates before overwriting โ€” both updates need the old values.
  4. Set current_max to the largest candidate and current_min to the smallest.
  5. Update max_product with current_max and return it at the end.
1def maxProduct(nums):
2    max_product = nums[0]
3    current_max = nums[0]
4    current_min = nums[0]
5
6    for num in nums[1:]:
7        # Compute all three candidates before overwriting anything
8        candidates = (num, current_max * num, current_min * num)
9        current_max = max(candidates)
10        current_min = min(candidates)
11        max_product = max(max_product, current_max)
12
13    return max_product

Time: O(n) โ€” a single pass through the array.
Space: O(1) โ€” three variables regardless of input size.

Complexity Summary

ApproachTimeSpaceWhen to use
Brute ForceO(nยฒ)O(1)Tiny inputs or when you need a simple verifier to test against
Track Max and MinO(n)O(1)Always โ€” same space cost, far faster

Common Mistakes

  • Updating current_max before computing current_min โ€” both calculations need the previous values of each other. If you write currentMax = ... first in Java or C++, currentMin will silently multiply by the freshly updated currentMax, producing the wrong answer. Always snapshot or compute all candidates together first.
  • Only tracking the maximum, not the minimum โ€” without current_min, you'd miss cases like [-3, -4] where the minimum running product (-3) multiplied by -4 gives the true maximum (12). The entire trick of this problem lives in that second variable.
  • Assuming zeros are a special case to handle separately โ€” they're not. The "element alone" candidate naturally handles zeros: max(0, prev_max ร— 0, prev_min ร— 0) = 0, so both trackers reset to 0 after a zero, which is exactly right.
  • Initializing max_product to 0 โ€” the input can be entirely negative, making the answer the least-negative single element (e.g., [-3, -5, -2] โ†’ -2). Initializing to 0 would incorrectly return 0 for inputs with no non-negative elements.
  • Confusing this with maximum sum subarray โ€” Kadane's algorithm for sums resets to the current element whenever the running sum goes negative. Copying that logic here and discarding a negative product is wrong: the product could be redeemed by a later negative, which the current_min tracker exists to capture.

Related Problems

  • maximum-subarray โ€” the closest cousin: same single-pass DP structure but for sums, where Kadane's reset suffices because sums can't be "redeemed" like products
  • best-time-to-buy-and-sell-stock โ€” also maintains a running best through one linear pass, tracking a global optimum updated at each step
  • subarray-sum-equals-k โ€” another subarray running-total problem, solved with prefix sums rather than state tracking
  • house-robber โ€” multi-state DP where each position depends on two previously computed values, paralleling the max/min dual-state here
  • longest-subarray-of-1s-after-deleting-one-element โ€” sliding window that tracks extra auxiliary state alongside the main window, same mental model as the dual tracker

Ready to practice? Try it on SkillFlow

Adaptive problems, AI follow-up interviews, and a skill score that shows exactly where you need to improve.

Practice This Problem โ†’