Maximum Product Subarray
Walk the array once, keeping both the largest and the smallest product that ends at each number, because a negative number swaps them. O(n) time and O(1) space, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”Given an array of integers nums, find the non-empty subarray, a run of numbers next to each other,
whose numbers multiply to the largest product. Return that product.
Example 1
Input: nums = [2, -5, 3, -2, 0, 4]
Output: 60
2 × −5 × 3 × −2 = 60. The two negatives cancel out. The 0 cuts the array, so nothing can reach past it.
Example 2
Input: nums = [-3, 0, -1]
Output: 0
Every subarray without the 0 is negative, so the 0 alone is best.
Example 3
Input: nums = [-2, -4, -3]
Output: 12
−4 × −3 = 12. Taking all three gives −24.
Constraints
Section titled “Constraints”- 1 ≤ nums.length ≤ 20,000 (call it n)
- −10 ≤ nums[i] ≤ 10
- The product of any subarray fits in a 32-bit integer.
Clarifying questions
Section titled “Clarifying questions”- Can the subarray be empty?
- No. It has at least one number, so a lone negative number can be the answer.
- Must the numbers be next to each other?
- Yes. A subarray is a run, not any pick of numbers.
- Can there be zeros?
- Yes. A zero makes any product through it 0.
- Can the product overflow?
- No. Every subarray product is promised to fit in 32 bits.
Spot the pattern
Section titled “Spot the pattern”The best subarray ending at each number can be built from the best one ending just before it. That is dynamic programming over positions, the same idea as Maximum Subarray, which sums instead. The twist is the sign: a negative number turns the smallest product into the largest. So we carry two answers, the largest and the smallest, instead of one.
Solutions
Section titled “Solutions”There is no middle approach worth its own tab. Splitting the array at zeros and counting negatives also gives O(n), but it needs more special cases than tracking two numbers.
Every subarray O(n²)
The idea
Section titled “The idea”Try every start i. From there, grow the subarray to the right one number at a time and multiply it in,
so each product reuses the one before instead of starting over. Keep the largest product seen.
Why it is correct: every subarray is some start and some end, and every pair is tried.
Walkthrough
Section titled “Walkthrough”- best = nums[0]
- for i in 0 .. len(nums) - 1:
- product = 1
- for j in i .. len(nums) - 1:
- product *= nums[j]; best = max(best, product)
- return best
Try every subarray. For each start i, grow the subarray to the right and multiply in one number at a time, so each new product reuses the one before.
class Solution { // Brute force: for every start, extend the subarray one number at a time and // keep a running product. Every subarray is tried once: O(n²) time, O(1) space. public int maxProduct(int[] nums) { int best = nums[0]; for (int i = 0; i < nums.length; i++) { int product = 1; for (int j = i; j < nums.length; j++) { // Reuse the product of nums[i..j-1] instead of starting over. product *= nums[j]; best = Math.max(best, product); } } return best; }}# Brute force: for every start, extend the subarray one number at a time and# keep a running product. Every subarray is tried once: O(n²) time, O(1) space.def max_product(nums: list[int]) -> int: best = nums[0] for i in range(len(nums)): product = 1 for j in range(i, len(nums)): # Reuse the product of nums[i..j-1] instead of starting over. product *= nums[j] best = max(best, product) return bestpublic class Solution{ // Brute force: for every start, extend the subarray one number at a time and // keep a running product. Every subarray is tried once: O(n²) time, O(1) space. public int MaxProduct(int[] nums) { int best = nums[0]; for (int i = 0; i < nums.Length; i++) { int product = 1; for (int j = i; j < nums.Length; j++) { // Reuse the product of nums[i..j-1] instead of starting over. product *= nums[j]; best = System.Math.Max(best, product); } } return best; }}// Brute force: for every start, extend the subarray one number at a time and// keep a running product. Every subarray is tried once: O(n²) time, O(1) space.func maxProduct(nums []int) int { best := nums[0] for i := range nums { product := 1 for j := i; j < len(nums); j++ { // Reuse the product of nums[i..j-1] instead of starting over. product *= nums[j] best = max(best, product) } } return best}// Brute force: for every start, extend the subarray one number at a time and// keep a running product. Every subarray is tried once: O(n²) time, O(1) space.function maxProduct(nums: number[]): number { let best = nums[0]; for (let i = 0; i < nums.length; i++) { let product = 1; for (let j = i; j < nums.length; j++) { // Reuse the product of nums[i..j-1] instead of starting over. product *= nums[j]; best = Math.max(best, product); } } return best;}1 Find the work that repeats
The inner loop does one multiplication and one max. That is a fixed job, so each run of the inner loop counts as 1 step.
- best = nums[0]
- for i in 0 .. len(nums) - 1:
- product = 1
- for j in i .. len(nums) - 1:
- product *= nums[j]; best = max(best, product)
- return best
2 Count the runs on Example 1
The table has one column per start. Start 0 grows to 6 numbers, start 5 has only itself.
6 + 5 + 4 + 3 + 2 + 1 = 21 runs.
start i 0 1 2 3 4 5 inner runs 6 5 4 3 2 1 total6 + 5 + 4 + 3 + 2 + 1 = 21 runs3 Write the count for any n
The inner loop runs n − i times for start i:
n + (n − 1) + … + 1 = n × (n + 1) / 2 runsCheck it with Example 1: 6 × 7 / 2 = 21. That matches the count.
4 Keep only the part that grows fastest
That is n² / 2 + n / 2. Drop the n / 2 and the 1/2. What is left is n². For 20,000 numbers, that is 200 million steps.
So the time is O(n²).
S Space: count the extra memory the same way
Only
bestand the running product. So the space is O(1).bestproduct
Largest and smallest so far O(n)
The idea
Section titled “The idea”Walk left to right and keep two numbers about the subarrays that end at the current number:
largest, the largest product of such a subarray, andsmallest, the smallest one, usually a big negative number.
For the next number, a subarray ending at number is either number alone, or a subarray ending just before
it with number added. Adding number multiplies the product by number. So the new largest is the largest of number,
largest × number and smallest × number, and the new smallest is the smallest of the three. Keep the best largest ever seen.
smallest matters because of signs. If number is negative, the smallest product times number becomes the largest.
number alone matters because of zeros: after a 0, both products are 0, and a fresh start is better.
Why it is correct: multiplying by number is largest either from the largest earlier product (when number is
positive) or from the smallest one (when number is negative). Keeping both means the best subarray ending at
every position is found, and the answer ends somewhere.
Walkthrough
Section titled “Walkthrough”- largest = smallest = best = nums[0]
- for number in nums[1:]:
- choices = (number, largest * number, smallest * number)
- largest = max(choices)
- smallest = min(choices)
- best = max(best, largest)
- return best
Walk left to right. largest and smallest are the largest and smallest product of a subarray that ends at the current number. Both start at 2.
class Solution { // Track the largest and smallest product ending here: a negative number turns // the smallest into the largest, so both are needed. One pass: O(n) time, O(1) space. public int maxProduct(int[] nums) { int largest = nums[0], smallest = nums[0], best = nums[0]; for (int i = 1; i < nums.length; i++) { int number = nums[i]; // Start fresh at x, or extend the best or worst run that ended just before. int withLargest = largest * number, withSmallest = smallest * number; largest = Math.max(number, Math.max(withLargest, withSmallest)); smallest = Math.min(number, Math.min(withLargest, withSmallest)); best = Math.max(best, largest); } return best; }}# Track the largest and smallest product ending here: a negative number turns# the smallest into the largest, so both are needed. One pass: O(n) time, O(1) space.def max_product(nums: list[int]) -> int: largest = smallest = best = nums[0] for i in range(1, len(nums)): number = nums[i] # Start fresh at x, or extend the best or worst run that ended just before. candidates = (number, largest * number, smallest * number) largest, smallest = max(candidates), min(candidates) best = max(best, largest) return bestpublic class Solution{ // Track the largest and smallest product ending here: a negative number turns // the smallest into the largest, so both are needed. One pass: O(n) time, O(1) space. public int MaxProduct(int[] nums) { int largest = nums[0], smallest = nums[0], best = nums[0]; for (int i = 1; i < nums.Length; i++) { int number = nums[i]; // Start fresh at x, or extend the best or worst run that ended just before. int withLargest = largest * number, withSmallest = smallest * number; largest = System.Math.Max(number, System.Math.Max(withLargest, withSmallest)); smallest = System.Math.Min(number, System.Math.Min(withLargest, withSmallest)); best = System.Math.Max(best, largest); } return best; }}// Track the largest and smallest product ending here: a negative number turns// the smallest into the largest, so both are needed. One pass: O(n) time, O(1) space.func maxProduct(nums []int) int { largest, smallest, best := nums[0], nums[0], nums[0] for _, number := range nums[1:] { // Start fresh at x, or extend the best or worst run that ended just before. withLargest, withSmallest := largest*number, smallest*number largest = max(number, withLargest, withSmallest) smallest = min(number, withLargest, withSmallest) best = max(best, largest) } return best}// Track the largest and smallest product ending here: a negative number turns// the smallest into the largest, so both are needed. One pass: O(n) time, O(1) space.function maxProduct(nums: number[]): number { let largest = nums[0]; let smallest = nums[0]; let best = nums[0]; for (let i = 1; i < nums.length; i++) { const number = nums[i]; // Start fresh at x, or extend the best or worst run that ended just before. const withLargest = largest * number; const withSmallest = smallest * number; largest = Math.max(number, withLargest, withSmallest); smallest = Math.min(number, withLargest, withSmallest); best = Math.max(best, largest); } return best;}1 Find the work that repeats
The loop body does two multiplications, a max, a min and one more max. That is a fixed job, so each run of the loop counts as 1 step.
- largest = smallest = best = nums[0]
- for number in nums[1:]:
- choices = (number, largest * number, smallest * number)
- largest = max(choices)
- smallest = min(choices)
- best = max(best, largest)
- return best
2 Count the runs on Example 1
The loop runs once for each number after the first.
5 runs.
index 1 2 3 4 5 loop runs 1 1 1 1 1 total5 runs3 Write the count for any n
n − 1 runsCheck it with Example 1: 6 − 1 = 5. That matches the count.
4 Keep only the part that grows fastest
Drop the − 1. What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
Three numbers, whatever n is. So the space is O(1).
largestsmallestbest
Tested at the limits
Section titled “Tested at the limits”2,000 random arrays of 1 to 10 numbers from −3 to 3, so zeros, sign flips and single negative numbers come up often. Every subarray and largest and smallest so far gave the same answer every time.
| Largest inputs | Result |
|---|---|
| 20,000 numbers (the most allowed): short runs of −2, −1, 1 and 2 between zeros, so the sign flips often. Largest and smallest so far. | Under 0.01 s, and under 0.001 MB of extra memory: just the two products and the best. |
| 2,000 numbers. Every subarray, about 2 million of them. | 0.08 s. At 20,000 numbers it would try 200 million, 100 times more. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.