Problem overview
maxProduct receives a list of integers nums and returns the largest product of a subarray. A subarray is a run of one or more elements that stand next to each other, and its product is all of them multiplied together.
For [2, 3, -2, 4] the answer is 6, from [2, 3]. The only negative number is -2, so every subarray that contains it has a negative product. For [-2, 0, -1] the answer is 0: [-2, -1] is not a subarray, because the 0 stands between the two numbers, and [0] gives 0. For [-2, 3, -4] the answer is 24, from the whole list, since the two negatives cancel out.
Approach: track the largest and the smallest product ending here
A maximum-sum scan keeps one running value. Products need two, because of negative numbers: multiplying by a negative flips the order, so the most negative product so far becomes the largest one after the next negative element. For each position, keep:
maxHere, the largest product of a subarray that ends at the current element;minHere, the smallest (most negative) such product;result, the largest product seen at any position so far.
At the next element x = nums[i], the best subarray ending there either starts fresh at x or extends the one ending at the previous element. If x is negative, the two running values swap roles first, because the old minimum times a negative is the new maximum. Starting fresh is also how the product gets past a 0: after a 0, extending gives 0, and the fresh start at the next element is larger.
Here is the run on [-2, 3, -4]. The first row is the starting values, and each other row shows the values after that element:
start maxHere=-2 minHere=-2 result=-2
i=1 x=3 maxHere=3 minHere=-6 result=3
i=2 x=-4 maxHere=24 minHere=-12 result=24
At x = -4 the two values swap first, so maxHere becomes -6 and minHere becomes 3. Then maxHere is the larger of -4 and -6 * -4 = 24, which is 24: the two negatives cancel across the whole list.
Step by step
- Start
maxHereatnums[0]. At the first element, the only subarray ending there is the element itself. - Start
minHereatnums[0]for the same reason. The smallest product matters because a negative number can turn it into the largest. - Start
resultatnums[0]. The subarray must have at least one element, so the first element alone is a valid candidate, even when it is negative. - Loop with
for i in range(1, len(nums)). Element 0 is already counted. - Swap on a negative. If
nums[i] < 0, domaxHere, minHere = minHere, maxHere. Afterwards each variable is the right one to multiply bynums[i]. - Update
maxHeretomax(nums[i], maxHere * nums[i]): start fresh or extend. - Update
minHeretomin(nums[i], minHere * nums[i]), the same two choices with the smaller one kept. - Update
resulttomax(result, maxHere). The best subarray ends at some element, so checking the best one ending at each element finds it. - Return
resultafter the loop.
The solution in Python
class Solution:
def maxProduct(self, nums: List[int]) -> int:
maxHere = nums[0]
minHere = nums[0]
result = nums[0]
for i in range(1, len(nums)):
if nums[i] < 0:
maxHere, minHere = minHere, maxHere
maxHere = max(nums[i], maxHere * nums[i])
minHere = min(nums[i], minHere * nums[i])
result = max(result, maxHere)
return result
Python details worth a closer look:
- Tuple swap.
maxHere, minHere = minHere, maxHereevaluates the right side first, so the two names exchange values without a temporary variable. - The order of the two updates. Both use the old values.
maxHereis changed first, butminHere's formula usesminHereandnums[i]only, so it isn't affected. WritingminHerewithmaxHerein its formula would be a bug. - Built-in
maxandmin. Each takes two or more arguments here and returns the larger or smaller one. - No overflow. Python integers grow as needed. The lesson guarantees every subarray's product fits in 32 bits anyway.
Complexity
Time: O(n). One pass over the list with a constant amount of work per element.
Space: O(1). Three variables, whatever the length of nums.
Edge cases and common mistakes
- A single element.
[-3]returns -3. Startingresultat 0 instead ofnums[0]would wrongly return 0. - Zeros split the list. In
[-2, 0, -1], at the 0 both running values become 0, so the earlier -2 can no longer combine with the later -1. The answer is 0. And[0, 2]returns 2: the fresh start after a 0 is what lets the product recover. - Keeping only the maximum. Tracking just
maxHerefails on[-2, 3, -4], where the best product comes from multiplying a very negative value by another negative. - An odd number of negatives.
[-2, -3, -4]returns 12, from[-3, -4]: the best subarray leaves one negative out. - Forgetting the fresh start. Using only
maxHere * nums[i]would keep multiplying by a 0 or a small value forever.
Testing
[2, 3, -2, 4]gives 6.[-2, 0, -1]gives 0.[-2, 3, -4]gives 24.[-3]gives -3.[0, 2]gives 2.[-2, -3, -4]gives 12.[2, -5, -2, -4, 3]gives 24.[1, -1, -1, 0, 5]gives 5.
