Facebook Pixel

152. Maximum Product Subarray

Problem Description

You are given an integer array nums. Your task is to find a contiguous subarray within this array that produces the largest product when all its elements are multiplied together, and return that maximum product.

A subarray is a contiguous sequence of elements from the array (it must contain at least one element). For example, if nums = [2, 3, -2, 4], some possible subarrays are [2], [2, 3], [3, -2, 4], and the entire array [2, 3, -2, 4].

The challenge involves handling both positive and negative numbers. Negative numbers can flip the sign of the product, which means a very negative product could become very positive if multiplied by another negative number.

For instance:

  • If nums = [2, 3, -2, 4], the subarray [2, 3] gives the maximum product of 6
  • If nums = [-2, 0, -1], the maximum product is 0 (from the subarray [0])
  • If nums = [-2, 3, -4], the maximum product is 24 (from the entire array, as -2 × 3 × -4 = 24)

The solution uses dynamic programming with two tracking variables:

  • f tracks the maximum product ending at the current position
  • g tracks the minimum product ending at the current position (important because a minimum negative value could become maximum when multiplied by another negative)

At each position, the algorithm considers three possibilities:

  1. Start a new subarray from the current element x
  2. Extend the previous maximum product by multiplying with x
  3. Extend the previous minimum product by multiplying with x (useful when x is negative)

The answer is guaranteed to fit within a 32-bit integer range.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that when dealing with products, negative numbers create an interesting dynamic - they can flip our results from very bad to very good (or vice versa). A large negative product becomes a large positive product when multiplied by another negative number.

Consider why a greedy approach of just keeping the maximum product won't work. If we have [-2, 3, -4], after processing -2 and 3, we'd have a maximum product of 3. But when we encounter -4, if we only tracked the maximum, we'd compute 3 × -4 = -12 and miss that -6 × -4 = 24 is actually better. The -6 came from -2 × 3, which was our minimum at that point.

This leads us to realize we need to track both the maximum AND minimum products ending at each position. Why? Because:

  • The minimum (most negative) product could become the maximum when multiplied by a negative number
  • The maximum product could become the minimum when multiplied by a negative number

At each position, we have three choices:

  1. Start fresh with just the current element (useful when previous products are worse than the element itself, like when we hit a 0 or when signs don't work in our favor)
  2. Extend the previous maximum by multiplying with the current element
  3. Extend the previous minimum by multiplying with the current element

By considering all three options and taking the appropriate max/min at each step, we ensure we don't miss any potential maximum product. The variables f and g act as our dynamic programming states, where f represents "what's the best product I can get ending here" and g represents "what's the worst product I can get ending here" - both of which might be useful for future calculations.

This approach elegantly handles all edge cases including zeros (which reset our product), negative numbers (which can flip our fortunes), and ensures we find the global maximum across all possible subarrays.

Solution Approach

The implementation uses a dynamic programming approach with space optimization, tracking two key values at each position:

  • f: maximum product ending at current position
  • g: minimum product ending at current position

Let's walk through the algorithm step by step:

Initialization:

ans = f = g = nums[0]

We initialize all three variables with the first element. ans will track our global maximum, while f and g track the current maximum and minimum products.

Main Loop:

for x in nums[1:]:

We iterate through the array starting from the second element.

Store Previous Values:

ff, gg = f, g

Before updating f and g, we store their previous values since we need them for calculations.

Update Maximum and Minimum:

f = max(x, ff * x, gg * x)
g = min(x, ff * x, gg * x)

For each element x, we calculate:

  • New maximum f: the largest among:
    • x (start new subarray)
    • ff * x (extend previous maximum)
    • gg * x (extend previous minimum - useful when x is negative)
  • New minimum g: the smallest among the same three values

Update Global Maximum:

ans = max(ans, f)

We continuously update our answer with the maximum product found so far.

Example Walkthrough with nums = [2, 3, -2, 4]:

  1. Initialize: ans = f = g = 2
  2. Process 3:
    • f = max(3, 2*3, 2*3) = 6
    • g = min(3, 2*3, 2*3) = 3
    • ans = max(2, 6) = 6
  3. Process -2:
    • f = max(-2, 6*(-2), 3*(-2)) = max(-2, -12, -6) = -2
    • g = min(-2, 6*(-2), 3*(-2)) = min(-2, -12, -6) = -12
    • ans = max(6, -2) = 6
  4. Process 4:
    • f = max(4, -2*4, -12*4) = max(4, -8, -48) = 4
    • g = min(4, -2*4, -12*4) = min(4, -8, -48) = -48
    • ans = max(6, 4) = 6

The algorithm correctly identifies that the maximum product subarray is [2, 3] with product 6.

Time Complexity: O(n) - single pass through the array
Space Complexity: O(1) - only using constant extra space

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with nums = [-2, 3, -4] to see how tracking both maximum and minimum values helps us find the optimal solution.

Initial State:

  • Start with the first element: ans = f = g = -2
  • f tracks maximum product ending here: -2
  • g tracks minimum product ending here: -2
  • ans tracks global maximum: -2

Step 1: Process element 3 (index 1)

  • Store previous values: ff = -2, gg = -2
  • Calculate new maximum: f = max(3, -2×3, -2×3) = max(3, -6, -6) = 3
    • We choose to start fresh with 3 rather than extend with -6
  • Calculate new minimum: g = min(3, -2×3, -2×3) = min(3, -6, -6) = -6
    • The minimum is -6 from extending the previous product
  • Update global max: ans = max(-2, 3) = 3

Step 2: Process element -4 (index 2)

  • Store previous values: ff = 3, gg = -6
  • Calculate new maximum: f = max(-4, 3×(-4), -6×(-4)) = max(-4, -12, 24) = 24
    • The key insight: multiplying the minimum -6 by negative -4 gives us 24!
  • Calculate new minimum: g = min(-4, 3×(-4), -6×(-4)) = min(-4, -12, 24) = -12
    • The minimum is now -12 from extending the previous maximum
  • Update global max: ans = max(3, 24) = 24

Result: The maximum product is 24 from the subarray [-2, 3, -4]

This example perfectly demonstrates why we need to track the minimum: at step 2, the previous minimum value -6 (which seemed "bad") became crucial for finding the maximum product when multiplied by the negative number -4. Without tracking the minimum, we would have missed this optimal solution and incorrectly returned 3.

Solution Implementation

1class Solution:
2    def maxProduct(self, nums: List[int]) -> int:
3        # Initialize: max_product tracks the overall maximum product found
4        # max_ending_here tracks the maximum product ending at current position
5        # min_ending_here tracks the minimum product ending at current position
6        max_product = max_ending_here = min_ending_here = nums[0]
7      
8        # Iterate through the array starting from the second element
9        for num in nums[1:]:
10            # Store previous values before updating
11            prev_max = max_ending_here
12            prev_min = min_ending_here
13          
14            # Update max_ending_here: it could be:
15            # 1. The current number itself (starting fresh)
16            # 2. Previous max times current (if both positive or both negative)
17            # 3. Previous min times current (if one is negative, could flip to max)
18            max_ending_here = max(num, prev_max * num, prev_min * num)
19          
20            # Update min_ending_here: similar logic but taking minimum
21            # We track minimum because a negative number could flip it to maximum later
22            min_ending_here = min(num, prev_max * num, prev_min * num)
23          
24            # Update the overall maximum product found so far
25            max_product = max(max_product, max_ending_here)
26      
27        return max_product
28
1class Solution {
2    public int maxProduct(int[] nums) {
3        // Initialize variables to track maximum and minimum products ending at current position
4        // We track both max and min because a negative number can flip them
5        int maxEndingHere = nums[0];  // Maximum product ending at current index
6        int minEndingHere = nums[0];  // Minimum product ending at current index
7        int globalMaxProduct = nums[0];  // Overall maximum product found so far
8      
9        // Iterate through the array starting from the second element
10        for (int i = 1; i < nums.length; i++) {
11            // Store previous values before updating
12            int previousMax = maxEndingHere;
13            int previousMin = minEndingHere;
14          
15            // Calculate new maximum product ending at current position
16            // It could be: 
17            // 1. Current element alone (start new subarray)
18            // 2. Previous max * current element (extend positive sequence)
19            // 3. Previous min * current element (negative * negative = positive)
20            maxEndingHere = Math.max(nums[i], 
21                                    Math.max(previousMax * nums[i], 
22                                            previousMin * nums[i]));
23          
24            // Calculate new minimum product ending at current position
25            // Similar logic but looking for minimum
26            minEndingHere = Math.min(nums[i], 
27                                    Math.min(previousMax * nums[i], 
28                                            previousMin * nums[i]));
29          
30            // Update global maximum if current maximum is larger
31            globalMaxProduct = Math.max(globalMaxProduct, maxEndingHere);
32        }
33      
34        return globalMaxProduct;
35    }
36}
37
1class Solution {
2public:
3    int maxProduct(vector<int>& nums) {
4        // Initialize: maxProd tracks maximum product ending at current position
5        // minProd tracks minimum product ending at current position (for handling negative numbers)
6        // result stores the global maximum product found so far
7        int maxProd = nums[0];
8        int minProd = nums[0];
9        int result = nums[0];
10      
11        // Iterate through the array starting from the second element
12        for (int i = 1; i < nums.size(); ++i) {
13            // Store previous values before updating
14            int prevMax = maxProd;
15            int prevMin = minProd;
16          
17            // Update maximum product ending at current position
18            // Consider three cases:
19            // 1. Start fresh with current number alone
20            // 2. Extend previous maximum product
21            // 3. Extend previous minimum product (useful when current number is negative)
22            maxProd = max({nums[i], prevMax * nums[i], prevMin * nums[i]});
23          
24            // Update minimum product ending at current position
25            // Similar logic but taking minimum to handle negative products
26            minProd = min({nums[i], prevMax * nums[i], prevMin * nums[i]});
27          
28            // Update global maximum result
29            result = max(result, maxProd);
30        }
31      
32        return result;
33    }
34};
35
1/**
2 * Finds the contiguous subarray within an array that has the largest product.
3 * Uses dynamic programming to track both maximum and minimum products at each position.
4 * 
5 * @param nums - Array of integers
6 * @returns The maximum product of any contiguous subarray
7 */
8function maxProduct(nums: number[]): number {
9    // Initialize variables to track maximum product, minimum product, and global maximum
10    let maxProductEndingHere: number = nums[0];
11    let minProductEndingHere: number = nums[0];
12    let globalMaxProduct: number = nums[0];
13  
14    // Iterate through the array starting from the second element
15    for (let i = 1; i < nums.length; i++) {
16        // Store current max and min before updating
17        const previousMax: number = maxProductEndingHere;
18        const previousMin: number = minProductEndingHere;
19      
20        // Calculate new maximum product ending at current position
21        // Consider three possibilities: current element alone, or multiply with previous max/min
22        maxProductEndingHere = Math.max(
23            nums[i], 
24            previousMax * nums[i], 
25            previousMin * nums[i]
26        );
27      
28        // Calculate new minimum product ending at current position
29        // Track minimum because negative * negative can become maximum
30        minProductEndingHere = Math.min(
31            nums[i], 
32            previousMax * nums[i], 
33            previousMin * nums[i]
34        );
35      
36        // Update global maximum product if current max is greater
37        globalMaxProduct = Math.max(globalMaxProduct, maxProductEndingHere);
38    }
39  
40    return globalMaxProduct;
41}
42

Time and Space Complexity

Time Complexity: O(n), where n is the length of the input array nums. The algorithm iterates through the array exactly once (starting from index 1), performing a constant number of operations for each element. The operations inside the loop (max, min, multiplication, and assignment) all take O(1) time.

Space Complexity: O(1). The algorithm uses only a fixed amount of extra space regardless of the input size. The variables ans, f, g, ff, and gg are the only additional storage used, and they don't scale with the input size. No additional data structures like arrays or recursion stacks are used.

Common Pitfalls

Pitfall 1: Not Tracking Both Maximum and Minimum

The Problem: A common mistake is only tracking the maximum product ending at each position, similar to the classic maximum subarray sum problem (Kadane's algorithm). This approach fails when dealing with negative numbers.

Why It Fails: Consider nums = [-2, 3, -4]. If we only track the maximum:

  • At -2: max = -2
  • At 3: max = max(3, -2 * 3) = 3
  • At -4: max = max(-4, 3 * -4) = -4

This gives us -4, but the actual answer is 24 (from multiplying all three numbers).

The Fix: Always track both maximum AND minimum products. The minimum (most negative) value can become the maximum when multiplied by a negative number:

# WRONG: Only tracking maximum
max_ending = nums[0]
result = nums[0]
for num in nums[1:]:
    max_ending = max(num, max_ending * num)
    result = max(result, max_ending)

# CORRECT: Tracking both maximum and minimum
max_ending = min_ending = result = nums[0]
for num in nums[1:]:
    temp_max = max_ending
    max_ending = max(num, max_ending * num, min_ending * num)
    min_ending = min(num, temp_max * num, min_ending * num)
    result = max(result, max_ending)

Pitfall 2: Forgetting to Store Previous Values Before Updating

The Problem: When updating max_ending_here and min_ending_here, you need the previous values of both. If you update max_ending_here first without storing its previous value, you'll use the wrong value when calculating min_ending_here.

Why It Fails:

# WRONG: Using updated max_ending_here to calculate min_ending_here
for num in nums[1:]:
    max_ending_here = max(num, max_ending_here * num, min_ending_here * num)
    min_ending_here = min(num, max_ending_here * num, min_ending_here * num)  # Bug!

In the second line, max_ending_here has already been updated, so we're not using the previous maximum.

The Fix: Store the previous values before any updates:

# CORRECT: Store previous values first
for num in nums[1:]:
    prev_max = max_ending_here
    prev_min = min_ending_here
    max_ending_here = max(num, prev_max * num, prev_min * num)
    min_ending_here = min(num, prev_max * num, prev_min * num)

Pitfall 3: Handling Arrays with Zeros Incorrectly

The Problem: Some implementations try to handle zeros as special cases by splitting the array at zero positions. While this can work, it adds unnecessary complexity.

Why It's Unnecessary: The standard algorithm handles zeros naturally. When we encounter a zero:

  • max_ending_here = max(0, prev_max * 0, prev_min * 0) = 0
  • min_ending_here = min(0, prev_max * 0, prev_min * 0) = 0

This effectively "resets" both trackers, which is exactly what we want - the product including a zero is always 0, so we essentially start fresh from the next element.

The Better Approach: Let the algorithm handle zeros naturally without special cases:

# No need for special zero handling
for num in nums[1:]:
    prev_max = max_ending_here
    prev_min = min_ending_here
    max_ending_here = max(num, prev_max * num, prev_min * num)
    min_ending_here = min(num, prev_max * num, prev_min * num)
    max_product = max(max_product, max_ending_here)

The algorithm automatically "breaks" the subarray at zeros and continues, making explicit zero-handling unnecessary.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More