Problem overview
maxProduct receives an array of integers, nums, and returns the largest product of a subarray: one or more elements that stand next to each other. The elements range from -10 to 10, so there are negatives and zeros to handle.
For [2, 3, -2, 4] the answer is 6, from [2, 3]: the -2 makes every subarray that contains it negative. For [-2, 0, -1] the answer is 0: the two negatives would give 2, but they aren't next to each other, because the 0 stands between them. For [-2, 3, -4] the answer is 24, the whole array, since the two negatives cancel.
Approach: track the largest and the smallest product
Trying every subarray takes O(n²) time or worse. A single pass is enough if, at each position, we know the best subarray that ends there. For sums, the largest sum ending at i is all you need. For products it isn't, because a negative number flips the order: the most negative product so far becomes the most positive after multiplying by a negative element.
So the pass keeps two numbers for the subarrays ending at the current element: maxHere, the largest product, and minHere, the smallest. At a negative element, the two swap roles before being multiplied. Each of them is then either the element alone, which starts a fresh subarray (and is how a product gets past a 0), or the old value times the element. A third variable, result, remembers the largest maxHere seen.
The trace for [2, 3, -2, 4], starting with all three variables at 2:
| i | nums[i] | maxHere | minHere | result |
|---|---|---|---|---|
| 1 | 3 | 6 | 3 | 6 |
| 2 | -2 | -2 | -12 | 6 |
| 3 | 4 | 4 | -48 | 6 |
For [-2, 3, -4], starting with all three at -2:
| i | nums[i] | maxHere | minHere | result |
|---|---|---|---|---|
| 1 | 3 | 3 | -6 | 3 |
| 2 | -4 | 24 | -12 | 24 |
At the last element, the -4 swaps the two values, so maxHere becomes -6, and -6 times -4 is 24.
Step by step
maxHere. Avarstarting atnums[0]: at the first element, the only subarray ending there is the element itself.minHere. Also starting atnums[0].result. Alsonums[0]. A subarray needs at least one element, so even a negative first element is a valid starting answer.- Loop
iover1..<nums.count, since element 0 is already counted. - Negative element. If
nums[i] < 0, swapmaxHereandminHerewith one tuple assignment. Afterwards each is the right one to multiply. - Update
maxHereto the larger ofnums[i]andmaxHere * nums[i]. - Update
minHereto the smaller ofnums[i]andminHere * nums[i]. - Update
resultto the larger ofresultandmaxHere. - Return
result.
The solution in Swift
class Solution {
func maxProduct(_ nums: [Int]) -> Int {
var maxHere = nums[0]
var minHere = nums[0]
var result = nums[0]
for i in 1..<nums.count {
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
}
}
The Swift details worth a closer look:
- Swapping with a tuple.
(maxHere, minHere) = (minHere, maxHere)builds a pair from the right side first and then unpacks it into the left side, so no temporary variable is needed. Both names must bevar. - The global
maxandmin. They are free functions that take two values. - Order of the updates.
minHereis computed from the oldminHere, not from the newmaxHere, so the two lines are independent. Both read the value after the swap. - Ranges and the single element.
1..<nums.countis empty when the array has one element, so[-3]returns -3. An empty array would crash onnums[0](and1..<0isn't a valid range); the lesson guarantees at least one element. - Overflow.
Intis 64 bits in Swift, and overflowing arithmetic crashes the program instead of wrapping. The lesson promises that every subarray product fits in 32 bits, so the products here stay small.
Complexity
Time: O(n). One pass, with a constant amount of work for each element.
Space: O(1). Three integers, whatever the length of the array.
Edge cases and common mistakes
- Tracking only the maximum. With only
maxHere, the array[-2, 3, -4]would give 3 instead of 24, since the -6 that the second negative needs would be lost. - Zeros. A 0 sends both products to 0 through
max(nums[i], ...)andmin(nums[i], ...), and the next element starts a new subarray.[-2, 0, -1]gives 0, and[0, 2]gives 2. - A single negative number.
[-3]gives -3, not 0. Startingresultat 0 would be a mistake. - All negatives.
[-2, -3, -4]gives 12 (the last two elements), and[-1, -1]gives 1. - Forgetting to swap before multiplying. The multiplication would use the wrong extreme for a negative element.
- Not allowing a fresh start. Using only
maxHere * nums[i]would let a product that has gone to 0 or below drag every later subarray down. Comparing withnums[i]alone lets the pass start over at any element. - Looking for the best at the end only. The best subarray can end anywhere, not only at the last element, so
resultis updated in every round. In[2, 3, -2, 4]the best product is already 6 after the second element and stays so.
Testing
[2, 3, -2, 4]gives 6.[-2, 0, -1]gives 0.[-2, 3, -4]gives 24.[-3]gives -3, and[0]gives 0.[-2, -3, -4]gives 12.[-1, -1]gives 1.[3, -1, 4]gives 4.
