← Back to DevBytes

Solving Maximum Product Subarray in Python: Step-by-Step Guide

Introduction to Maximum Product Subarray

The Maximum Product Subarray problem is a classic algorithmic challenge that frequently appears in coding interviews and competitive programming. It tests a developer's understanding of dynamic programming, array manipulation, and edge case handling. Unlike the Maximum Subarray Sum problem (Kadane's algorithm), the product variant introduces additional complexity due to the behavior of negative numbers and zeros.

What is the Maximum Product Subarray?

The Maximum Product Subarray problem asks you to find the contiguous subarray within a given array of integers that has the largest product. A subarray is a contiguous part of the array. For example, given the array [2, 3, -2, 4], the subarray [2, 3] has the largest product of 6.

The challenge arises because multiplying two negative numbers produces a positive result. This means a seemingly "bad" negative product can suddenly become the maximum when multiplied by another negative number. Additionally, zeros in the array reset the product, requiring careful handling.

Why It Matters

Understanding this problem is crucial for several reasons:

Understanding the Problem

Problem Statement

Given an integer array nums, find a contiguous non-empty subarray within the array that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer.

Let's break down the key components:

Examples to Build Intuition

Let's examine several examples to understand the problem better:

Example 1:

Input: nums = [2, 3, -2, 4]
Output: 6
Explanation: [2, 3] has the largest product 6.

Example 2:

Input: nums = [-2, 0, -1]
Output: 0
Explanation: The result cannot be 2, because [-2, -1] is not a subarray.

Example 3 (with negative numbers):

Input: nums = [-2, 3, -4]
Output: 24
Explanation: [-2, 3, -4] has the largest product 24.

Notice in Example 3 how the entire array produces the maximum product because the two negative numbers multiply to give a positive result, which when multiplied by 3 gives 24.

Approaches to Solve the Problem

Brute Force Approach

The most straightforward approach is to consider all possible subarrays and compute their products. While simple to understand, this approach has significant limitations.

def maxProductBruteForce(nums):
    n = len(nums)
    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 the brute force solution
print(maxProductBruteForce([2, 3, -2, 4]))  # Output: 6
print(maxProductBruteForce([-2, 0, -1]))    # Output: 0

Time Complexity: O(n²) - We have nested loops iterating through the array.

Space Complexity: O(1) - We only use a constant amount of extra space.

While this works for small arrays, it becomes inefficient for large inputs. We need a more optimized solution.

Dynamic Programming Approach

The key insight for an optimized solution is that we need to track both the maximum and minimum products at each position. Why? Because a minimum (most negative) product can become the maximum when multiplied by another negative number.

Here's the logic:

Optimized DP Approach

We can optimize the space complexity by only storing the previous maximum and minimum values rather than entire arrays. This brings space complexity down to O(1) while maintaining O(n) time complexity.

Step-by-Step Implementation

Setting Up the Solution

Let's implement the optimized dynamic programming solution step by step. First, we'll handle the base case and initialize our tracking variables:

def maxProduct(nums):
    # Handle empty array edge case
    if not nums:
        return 0
    
    # Initialize variables
    # max_prod tracks maximum product ending at current position
    # min_prod tracks minimum product ending at current position
    # result tracks the overall maximum product found
    max_prod = nums[0]
    min_prod = nums[0]
    result = nums[0]
    
    return result

Complete Implementation

Now let's build the complete solution with the main loop:

def maxProduct(nums):
    """
    Find the maximum product of a contiguous subarray.
    
    Args:
        nums: List of integers
        
    Returns:
        int: Maximum product of any contiguous subarray
    """
    # Handle empty array edge case
    if not nums:
        return 0
    
    # Initialize tracking variables
    max_prod = nums[0]  # Maximum product ending at current position
    min_prod = nums[0]  # Minimum product ending at current position
    result = nums[0]    # Overall maximum product found
    
    # Iterate through the array starting from the second element
    for i in range(1, len(nums)):
        current = nums[i]
        
        # When current is negative, multiplying by max_prod gives a smaller number
        # and multiplying by min_prod gives a larger number
        # So we swap max_prod and min_prod when current is negative
        if current < 0:
            max_prod, min_prod = min_prod, max_prod
        
        # Update max_prod and min_prod for current position
        # Either start a new subarray at current, or extend the previous subarray
        max_prod = max(current, max_prod * current)
        min_prod = min(current, min_prod * current)
        
        # Update the overall result
        result = max(result, max_prod)
    
    return result

Testing the Solution

Let's test our solution with various test cases to ensure it handles all scenarios correctly:

# Test case 1: Basic positive numbers
print(maxProduct([2, 3, -2, 4]))  # Expected: 6

# Test case 2: Array with zero
print(maxProduct([-2, 0, -1]))    # Expected: 0

# Test case 3: All negative numbers
print(maxProduct([-2, 3, -4]))    # Expected: 24

# Test case 4: Single element
print(maxProduct([5]))            # Expected: 5

# Test case 5: Single negative element
print(maxProduct([-3]))           # Expected: -3

# Test case 6: All negative numbers (even count)
print(maxProduct([-1, -2, -3, -4]))  # Expected: 24

# Test case 7: Mixed with zeros
print(maxProduct([0, 2, 3, 0, 4, 5]))  # Expected: 20

# Test case 8: Large negative then positive
print(maxProduct([-4, -3, -2]))   # Expected: 12

# Test case 9: Two elements
print(maxProduct([2, -5]))        # Expected: 2

# Test case 10: Alternating signs
print(maxProduct([1, -2, 3, -4, 5, -6]))  # Expected: 720

Tracing Through an Example

To fully understand how the algorithm works, let's trace through the array [-2, 3, -4]:

Initial state:
  max_prod = -2
  min_prod = -2
  result = -2

Iteration 1 (current = 3):
  current is positive, no swap
  max_prod = max(3, -2 * 3) = max(3, -6) = 3
  min_prod = min(3, -2 * 3) = min(3, -6) = -6
  result = max(-2, 3) = 3

Iteration 2 (current = -4):
  current is negative, swap max_prod and min_prod
  max_prod = -6, min_prod = 3
  max_prod = max(-4, -6 * -4) = max(-4, 24) = 24
  min_prod = min(-4, 3 * -4) = min(-4, -12) = -12
  result = max(3, 24) = 24

Final result: 24

Alternative Implementation

There's another elegant approach that traverses the array from both directions. This works because the maximum product subarray will either be in the prefix or suffix when there's an odd number of negative numbers:

def maxProductBidirectional(nums):
    """
    Alternative approach: traverse from both directions.
    
    Args:
        nums: List of integers
        
    Returns:
        int: Maximum product of any contiguous subarray
    """
    if not nums:
        return 0
    
    max_product = nums[0]
    product = 1
    
    # Forward pass
    for i in range(len(nums)):
        product *= nums[i]
        max_product = max(max_product, product)
        if nums[i] == 0:
            product = 1
    
    product = 1
    # Backward pass
    for i in range(len(nums) - 1, -1, -1):
        product *= nums[i]
        max_product = max(max_product, product)
        if nums[i] == 0:
            product = 1
    
    return max_product

# Test the bidirectional approach
print(maxProductBidirectional([2, 3, -2, 4]))  # Output: 6
print(maxProductBidirectional([-2, 0, -1]))    # Output: 0
print(maxProductBidirectional([-2, 3, -4]))    # Output: 24

This approach is intuitive because:

Best Practices

Edge Case Handling

Always consider and test edge cases when implementing this solution:

Code Readability and Documentation

Write clean, well-documented code:

def maxProduct(nums):
    """
    Find the contiguous subarray with the largest product.
    
    Uses dynamic programming to track both maximum and minimum
    products at each position, handling negative numbers by
    swapping max and min when encountering a negative value.
    
    Time Complexity: O(n)
    Space Complexity: O(1)
    
    Args:
        nums (List[int]): Input array of integers
        
    Returns:
        int: Maximum product of any contiguous subarray
        
    Raises:
        ValueError: If input array is empty
        
    Examples:
        >>> maxProduct([2, 3, -2, 4])
        6
        >>> maxProduct([-2, 0, -1])
        0
    """
    if not nums:
        raise ValueError("Input array cannot be empty")
    
    max_ending_here = nums[0]
    min_ending_here = nums[0]
    max_so_far = nums[0]
    
    for i in range(1, len(nums)):
        num = nums[i]
        
        if num < 0:
            max_ending_here, min_ending_here = min_ending_here, max_ending_here
        
        max_ending_here = max(num, max_ending_here * num)
        min_ending_here = min(num, min_ending_here * num)
        
        max_so_far = max(max_so_far, max_ending_here)
    
    return max_so_far

Performance Considerations

Testing Strategy

Develop a comprehensive testing strategy:

import unittest

class TestMaxProduct(unittest.TestCase):
    def test_basic_positive(self):
        self.assertEqual(maxProduct([2, 3, -2, 4]), 6)
    
    def test_with_zero(self):
        self.assertEqual(maxProduct([-2, 0, -1]), 0)
    
    def test_all_negative_odd(self):
        self.assertEqual(maxProduct([-2, 3, -4]), 24)
    
    def test_single_element(self):
        self.assertEqual(maxProduct([5]), 5)
    
    def test_single_negative(self):
        self.assertEqual(maxProduct([-3]), -3)
    
    def test_all_negative_even(self):
        self.assertEqual(maxProduct([-1, -2, -3, -4]), 24)
    
    def test_mixed_with_zeros(self):
        self.assertEqual(maxProduct([0, 2, 3, 0, 4, 5]), 20)
    
    def test_empty_array(self):
        with self.assertRaises(ValueError):
            maxProduct([])
    
    def test_all_zeros(self):
        self.assertEqual(maxProduct([0, 0, 0]), 0)
    
    def test_large_array(self):
        nums = [1] * 10000
        self.assertEqual(maxProduct(nums), 1)

if __name__ == '__main__':
    unittest.main()

Common Mistakes to Avoid

Forgetting to Track Minimum Product

A common mistake is only tracking the maximum product, similar to Kadane's algorithm for maximum subarray sum. This fails because it doesn't account for negative numbers turning into large positive products:

# INCORRECT - Only tracking maximum
def maxProductWrong(nums):
    if not nums:
        return 0
    
    max_prod = nums[0]
    result = nums[0]
    
    for i in range(1, len(nums)):
        # This doesn't handle negative numbers correctly!
        max_prod = max(nums[i], max_prod * nums[i])
        result = max(result, max_prod)
    
    return result

# This will give wrong answer for [-2, 3, -4]
print(maxProductWrong([-2, 3, -4]))  # Output: 3 (wrong, should be 24)

Not Handling the Swap Correctly

Another mistake is performing the swap at the wrong time or not swapping at all:

# INCORRECT - Swap at wrong time
def maxProductWrongSwap(nums):
    if not nums:
        return 0
    
    max_prod = min_prod = result = nums[0]
    
    for i in range(1, len(nums)):
        num = nums[i]
        
        # Wrong: updating before swap uses stale values
        max_prod = max(num, max_prod * num)
        min_prod = min(num, min_prod * num)
        
        if num < 0:
            max_prod, min_prod = min_prod, max_prod
        
        result = max(result, max_prod)
    
    return result

Conclusion

The Maximum Product Subarray problem is an excellent example of how dynamic programming can transform a seemingly complex problem into an elegant O(n) solution. By tracking both maximum and minimum products at each position and understanding how negative numbers interact through multiplication, we can efficiently solve this problem with optimal time and space complexity. The key insight—that a minimum product can become a maximum when multiplied by a negative number—is what differentiates this problem from its simpler cousin, the Maximum Subarray Sum problem. Whether you choose the DP approach with min/max tracking or the bidirectional traversal method, both provide efficient solutions. Remember to always test your implementation against edge cases including empty arrays, single elements, all-negative arrays, and arrays containing zeros. With the techniques covered in this tutorial, you're now equipped to tackle this problem confidently in both interviews and real-world applications.

🛠 Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts · $19
AI Dev Kit for Mac — local AI dev environment templates · $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit · $7.99

← Back to all articles