← Back to DevBytes

Maximum Product Subarray: Multiple Solutions and Complexity Analysis

Understanding the Maximum Product Subarray Problem

The Maximum Product Subarray problem asks us to find the contiguous subarray within a given integer array that yields the largest product. Unlike the classic Maximum Subarray Sum (Kadane's algorithm), product introduces unique challenges: negative numbers can flip a small product into a large one when multiplied together, and zeros reset everything to zero.

Formal statement: Given an integer array nums of size n, find the contiguous subarray (containing at least one element) that has the largest product, and return that product.

Example:

Input:  nums = [2, 3, -2, 4]
Output: 6
Explanation: The subarray [2, 3] gives product 6.

Input:  nums = [-2, 0, -1]
Output: 0
Explanation: [0] or [-2, 0] both yield 0, which is the maximum here.

Input:  nums = [-2, 3, -4]
Output: 24
Explanation: The entire array [-2, 3, -4] gives product 24, 
because -2 * 3 = -6, then -6 * -4 = 24.

Why This Problem Matters

This problem appears frequently in coding interviews at companies like Google, Amazon, and Meta. Beyond interviews, it teaches fundamental concepts that apply to real-world scenarios:

Solution 1: Brute Force β€” Examine All Subarrays

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

The most straightforward approach computes the product of every possible contiguous subarray and tracks the maximum. While simple to implement, its cubic or quadratic complexity makes it impractical for large inputs.

Approach

For each starting index i, iterate over all ending indices j β‰₯ i, compute the product nums[i] * nums[i+1] * ... * nums[j], and update the global maximum.

Implementation (Python)

def max_product_brute_force(nums):
    n = len(nums)
    if n == 0:
        return 0
    
    max_product = nums[0]
    
    for i in range(n):
        current_product = 1
        for j in range(i, n):
            current_product *= nums[j]
            max_product = max(max_product, current_product)
    
    return max_product

# Test cases
print(max_product_brute_force([2, 3, -2, 4]))   # 6
print(max_product_brute_force([-2, 0, -1]))      # 0
print(max_product_brute_force([-2, 3, -4]))      # 24
print(max_product_brute_force([0, 2]))            # 2

Complexity Analysis

This approach is acceptable for n ≀ 500 but times out on larger arrays. More importantly, it recalculates products from scratch for each subarray instead of reusing work, which we can optimize.

Solution 2: Optimized Brute Force with Prefix Products

We can precompute prefix products and then derive any subarray product in O(1) using division. However, this fails when zeros appear in the array (division by zero) and requires careful handling of floating-point precision for large products. For integer arrays, it's generally cleaner to avoid division.

Let's move directly to the optimal dynamic programming solution that handles all edge cases elegantly.

Solution 3: Dynamic Programming β€” Tracking Both Minimum and Maximum

The key insight is that a negative number multiplied by a minimum (most negative) product can yield a maximum product. For example, if our current running minimum is -12 and we encounter -3, the new product becomes 36β€”which could be the new maximum. Therefore, at each step we must track both the maximum and minimum product ending at the current position.

Algorithm Walkthrough

Let max_ending_here be the maximum product of any subarray ending at index i, and min_ending_here be the minimum product ending at index i. When we process nums[i], we consider three candidates:

We update max_ending_here and min_ending_here accordingly, then update the global result with the best max_ending_here seen so far.

Detailed Step-by-Step Dry Run

Consider nums = [2, 3, -2, 4]:

Index 0, value = 2:
  max_ending = 2, min_ending = 2, result = 2

Index 1, value = 3:
  Candidates: 3, 2*3=6, 2*3=6
  max_ending = 6, min_ending = 3, result = 6

Index 2, value = -2:
  Candidates: -2, 6*(-2)=-12, 3*(-2)=-6
  max_ending = max(-2, -12, -6) = -2
  min_ending = min(-2, -12, -6) = -12
  result stays 6 (since -2 < 6)

Index 3, value = 4:
  Candidates: 4, (-2)*4=-8, (-12)*4=-48
  max_ending = max(4, -8, -48) = 4
  min_ending = min(4, -8, -48) = -48
  result stays 6

Final answer: 6

Now consider nums = [-2, 3, -4] where the whole array product is positive:

Index 0, value = -2:
  max_ending = -2, min_ending = -2, result = -2

Index 1, value = 3:
  Candidates: 3, (-2)*3=-6, (-2)*3=-6
  max_ending = 3, min_ending = -6, result = 3

Index 2, value = -4:
  Candidates: -4, 3*(-4)=-12, (-6)*(-4)=24
  max_ending = max(-4, -12, 24) = 24
  min_ending = min(-4, -12, 24) = -12
  result = 24

Final answer: 24 βœ“

Implementation (Python) β€” Clean and Production-Ready

def max_product_subarray(nums):
    """
    Returns the maximum product of any contiguous subarray.
    Handles negatives, zeros, and single-element arrays.
    Time: O(n), Space: O(1)
    """
    if not nums:
        return 0
    
    # Initialize with the first element
    max_ending_here = nums[0]
    min_ending_here = nums[0]
    result = nums[0]
    
    for i in range(1, len(nums)):
        current = nums[i]
        
        # Compute candidates for max and min ending here
        # We multiply current with both previous max and min
        candidate1 = current
        candidate2 = max_ending_here * current
        candidate3 = min_ending_here * current
        
        # The new max ending here is the maximum of all three
        max_ending_here = max(candidate1, candidate2, candidate3)
        # The new min ending here is the minimum of all three
        min_ending_here = min(candidate1, candidate2, candidate3)
        
        # Update global maximum
        result = max(result, max_ending_here)
    
    return result

# Verification with edge cases
test_cases = [
    ([2, 3, -2, 4], 6),
    ([-2, 0, -1], 0),
    ([-2, 3, -4], 24),
    ([0, 2], 2),
    ([-2], -2),
    ([2, -5, -2, -4, 3], 24),  # [-2, -4, 3]? Actually [-5,-2,-4,3] = -120? Let's trace
    ([2, -5, -2, -4, 3], 120),  # 2*(-5)*(-2)*(-4)*3 = -240? Wait: 2*(-5)=-10, *(-2)=20, *(-4)=-80, *3=-240. The max is 20? Or 2*(-5)*(-2)=20. Let's check: max is 20? Actually -5*-2*-4*3 = -120, 2*-5*-2 = 20. Max = 20. So expected 20.
]

for nums, expected in test_cases:
    result = max_product_subarray(nums)
    status = "βœ“" if result == expected else f"βœ— (got {result}, expected {expected})"
    print(f"{status} | {nums} -> {result}")

Let me correct that last test case with a proper verification:

nums = [2, -5, -2, -4, 3]

# Let's find all subarray products manually:
# [2] = 2
# [2, -5] = -10
# [2, -5, -2] = 20        ← candidate max
# [2, -5, -2, -4] = -80
# [2, -5, -2, -4, 3] = -240
# [-5] = -5
# [-5, -2] = 10
# [-5, -2, -4] = -40
# [-5, -2, -4, 3] = -120
# [-2] = -2
# [-2, -4] = 8
# [-2, -4, 3] = -24
# [-4] = -4
# [-4, 3] = -12
# [3] = 3

# Maximum product = 20
print(max_product_subarray([2, -5, -2, -4, 3]))  # Should output 20

Complexity Analysis

This is optimal. No algorithm can do better than O(n) because we must at least inspect each element once to determine if it contributes to the maximum product.

Solution 4: Alternative Two-Pass Approach (Left-to-Right and Right-to-Left)

Another elegant solution exploits the observation that a maximum product subarray either does not cross a zero, and if it has an odd number of negatives, the maximum product lies entirely to the left or right of one negative element. By making two passes (one from left, one from right), we can capture all possibilities without explicitly tracking min/max.

How It Works

We maintain a running product. When we hit a zero, we reset the running product to 1 (since zero breaks any product chain). We do this in both directions and take the maximum seen. This works because:

Implementation (Python)

def max_product_two_pass(nums):
    if not nums:
        return 0
    
    n = len(nums)
    max_product = nums[0]
    
    # Left-to-right pass
    current_product = 1
    for i in range(n):
        current_product *= nums[i]
        max_product = max(max_product, current_product)
        if nums[i] == 0:
            current_product = 1  # reset after zero
    
    # Right-to-left pass
    current_product = 1
    for i in range(n - 1, -1, -1):
        current_product *= nums[i]
        max_product = max(max_product, current_product)
        if nums[i] == 0:
            current_product = 1  # reset after zero
    
    return max_product

# Test
print(max_product_two_pass([2, 3, -2, 4]))      # 6
print(max_product_two_pass([-2, 0, -1]))         # 0
print(max_product_two_pass([-2, 3, -4]))         # 24
print(max_product_two_pass([2, -5, -2, -4, 3]))  # 20
print(max_product_two_pass([0]))                  # 0
print(max_product_two_pass([-1]))                 # -1

Why Reset to 1 and Not 0?

Resetting to 1 after a zero is crucial. If we reset to 0, all subsequent multiplications would stay 0, losing information. By resetting to 1 (the multiplicative identity), we effectively start a fresh product computation for the next non-zero segment. The zero itself is already considered when we update max_product before the reset.

Complexity Analysis

This approach is slightly simpler to reason about and less prone to off-by-one errors, though it does require understanding the prefix/suffix property of negative products.

Solution 5: Kadane-Style with Explicit Swap on Negative

Yet another variation swaps the min and max when a negative number is encountered, because multiplying a negative flips the ordering of products. This is a concise and elegant implementation.

def max_product_swap_approach(nums):
    if not nums:
        return 0
    
    max_ending = nums[0]
    min_ending = nums[0]
    result = nums[0]
    
    for i in range(1, len(nums)):
        current = nums[i]
        
        # When current is negative, swapping max and min
        # ensures we multiply the most negative (now max_ending after swap)
        # by current to potentially get a large positive.
        if current < 0:
            max_ending, min_ending = min_ending, max_ending
        
        # max_ending is now the larger of starting fresh or extending
        max_ending = max(current, max_ending * current)
        # min_ending is the smaller of starting fresh or extending
        min_ending = min(current, min_ending * current)
        
        result = max(result, max_ending)
    
    return result

# Verify
print(max_product_swap_approach([2, 3, -2, 4]))      # 6
print(max_product_swap_approach([-2, 0, -1]))         # 0
print(max_product_swap_approach([-2, 3, -4]))         # 24

The swap trick works because after swapping, max_ending holds the previous minimum (most negative), and multiplying by a negative current can yield a large positive. Meanwhile, min_ending holds the previous maximum, which when multiplied by a negative becomes the new minimum. This is mathematically equivalent to the three-candidate approach but arguably more elegant.

Edge Cases and Pitfalls

Single Negative Element

When the array contains only one negative number, say [-7], the maximum product is -7 itself. All algorithms above correctly handle this because we initialize with the first element and never reset below it without considering it.

All Zeros

For [0, 0, 0], the maximum product is 0. The two-pass approach resets on each zero and correctly returns 0. The DP approach keeps max_ending and min_ending at 0 and never goes negative.

Overflow in Languages with Fixed-Size Integers

In languages like Java or C++, multiplying large numbers can cause integer overflow. Python handles arbitrary-precision integers natively, so overflow is not a concern. For production code in languages with 32-bit or 64-bit limits, consider using long long or BigInteger equivalents, or detect overflow and clamp. In interview settings, mention this awareness.

Empty Array

Always handle the empty input case. The function should return 0, raise an exception, or return a sentinel value based on requirements. The code examples above return 0 for empty arrays.

Comparative Performance Table

Here is a summary of all approaches discussed:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Approach             β”‚ Time        β”‚ Space         β”‚ Notes            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Brute Force          β”‚ O(nΒ²)       β”‚ O(1)          β”‚ Simple, slow     β”‚
β”‚ Prefix Division      β”‚ O(n)        β”‚ O(n)          β”‚ Fails on zeros   β”‚
β”‚ DP (min/max track)   β”‚ O(n)        β”‚ O(1)          β”‚ Optimal, robust  β”‚
│ Two-Pass (L→R, R→L)  │ O(n)        │ O(1)          │ Elegant, simple  │
β”‚ Swap on Negative     β”‚ O(n)        β”‚ O(1)          β”‚ Concise, clever  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Best Practices for Implementation

Real-World Applications and Extensions

Financial Data: Maximum Compound Return

Given daily returns as multipliers (e.g., 1.02 for +2%, 0.98 for -2%), finding the subarray with maximum product identifies the holding period with best cumulative return. This directly maps to the problem, with the twist that all values are positive (no sign flips), which simplifies the solution to standard Kadane on log-transformed data.

Signal Envelope Detection

In audio processing, finding the maximum amplitude envelope over a sliding window can use product-based metrics when combining frequency-domain magnitudes. The ability to handle negative coefficients makes the min/max tracking approach particularly valuable.

Machine Learning: Gradient-Based Optimization

When tracking the product of gradients across time steps (in recurrent networks or attention mechanisms), understanding whether the accumulated product is exploding (large magnitude, positive or negative) helps in diagnosing training instability. The min/max tracking pattern appears in implementations of gradient clipping and normalization.

Extension: Maximum Product Subarray in a 2D Matrix

The 1D solution serves as a building block for the 2D version, where you fix two row boundaries, compress the columns into a 1D array (via multiplication across rows), and run the 1D algorithm. This is O(mΒ²Β·n) for an mΓ—n matrix, using the same min/max tracking per compression.

Conclusion

The Maximum Product Subarray problem is a beautiful illustration of how a seemingly simple twistβ€”replacing addition with multiplicationβ€”fundamentally changes the nature of an optimization problem. The presence of negative numbers forces us to abandon a pure greedy approach and instead maintain dual state (minimum and maximum), a pattern that recurs across many advanced algorithm problems. We explored five distinct solutions, from the obvious O(nΒ²) brute force to the optimal O(n) dynamic programming and two-pass approaches, each with its own pedagogical value. The optimal solutions run in linear time with constant space, are robust to all edge cases including zeros and single-element arrays, and translate cleanly across programming languages. When you encounter a problem involving products, XOR, or any operation where "negative" values can invert ordering, remember this pattern: track both the maximum and minimum ending here. It will serve you well in interviews and real-world systems alike.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles