title: Maximum Product Subarray
slug: maximum-product-subarray
difficulty: medium
leetcode_id: 152
leetcode_url: https://leetcode.com/problems/maximum-product-subarray/
categories:
  - arrays
  - dynamic-programming
patterns:
  - dynamic-programming

function_signature: "def max_product(nums: list[int]) -> int:"

test_cases:
  visible:
    - input: { nums: [2, 3, -2, 4] }
      expected: 6
    - input: { nums: [-2, 0, -1] }
      expected: 0
    - input: { nums: [2, -5, -2, -4, 3] }
      expected: 24
  hidden:
    - input: { nums: [0] }
      expected: 0
    - input: { nums: [-2] }
      expected: -2
    - input: { nums: [1, 2, 3, 4] }
      expected: 24
    - input: { nums: [-1, -2, -3] }
      expected: 6
    - input: { nums: [0, 2] }
      expected: 2
    - input: { nums: [-2, 3, -4] }
      expected: 24
    - input: { nums: [2, -1, 1, 1] }
      expected: 2

description: |
  Given an integer array `nums`, find a subarray 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.

  **Note** that the product of an array with a single element is the value of that element.

constraints: |
  - `1 <= nums.length <= 2 * 10^4`
  - `-10 <= nums[i] <= 10`
  - The product of any subarray of `nums` is **guaranteed** to fit in a **32-bit** integer.

examples:
  - input: "nums = [2,3,-2,4]"
    output: "6"
    explanation: "The subarray [2,3] has the largest product 6."
  - input: "nums = [-2,0,-1]"
    output: "0"
    explanation: "The result cannot be 2, because [-2,-1] is not a subarray (elements must be contiguous)."

explanation:
  intuition: |
    This problem is a twist on the classic **Maximum Subarray Sum** (Kadane's algorithm). However, products behave differently from sums in one crucial way: **multiplying by a negative number flips the sign**.

    Imagine you're tracking the largest product ending at each position. If you encounter a negative number, your current large positive product suddenly becomes a large *negative* product. But here's the twist: if you encounter *another* negative number later, that large negative product could flip back to become the largest positive product!

    Think of it like this: negative numbers are "wild cards" that can transform your worst result into your best result. A very negative product multiplied by another negative number becomes a very positive product.

    The key insight is that we need to track **both** the maximum and minimum products ending at each position:
    - The **maximum** product could come from extending the previous maximum (if current is positive) or from the previous minimum flipping sign
    - The **minimum** product matters because it might become the maximum after hitting a negative number

    This dual-tracking approach lets us handle the sign-flipping nature of multiplication in a single pass.

  approach: |
    We solve this using a **Dynamic Programming** approach that tracks both maximum and minimum products:

    **Step 1: Initialise variables**

    - `max_product`: Set to `nums[0]` — the global maximum product found
    - `current_max`: Set to `nums[0]` — the maximum product ending at the current position
    - `current_min`: Set to `nums[0]` — the minimum product ending at the current position (needed for negative flips)

    &nbsp;

    **Step 2: Iterate through the array starting from index 1**

    For each element `num`:

    - If `num` is negative, swap `current_max` and `current_min` — this prepares for the sign flip
    - Update `current_max`: the larger of `num` alone (start fresh) or `current_max * num` (extend)
    - Update `current_min`: the smaller of `num` alone (start fresh) or `current_min * num` (extend)
    - Update `max_product` if `current_max` is greater

    &nbsp;

    **Step 3: Return the result**

    - Return `max_product` — the largest product subarray found

    &nbsp;

    The swap trick before updating handles the sign flip elegantly: when we multiply by a negative, what was maximum becomes minimum and vice versa.

  common_pitfalls:
    - title: Ignoring Negative Numbers
      description: |
        A common mistake is to apply Kadane's algorithm directly without modification:
        ```python
        current_max = max(num, current_max * num)
        ```

        This fails because it discards negative products that could become positive later.

        For example, with `nums = [2, 3, -2, 4, -1]`:
        - Without tracking minimum: After `-2`, you'd start fresh with `4`, missing that `2 * 3 * -2 * 4 * -1 = 48`
        - The two negatives cancel out, giving a larger product than any positive-only subarray
      wrong_approach: "Only track maximum product (standard Kadane's)"
      correct_approach: "Track both maximum AND minimum products"

    - title: Forgetting Zeros Reset Everything
      description: |
        When you encounter `0`, any product including it becomes `0`. The subarray must either:
        - End before the zero
        - Start after the zero
        - Be just `[0]` if everything else is negative

        The algorithm handles this naturally: `max(num, current_max * num)` will choose `0` (starting fresh) when `current_max * 0 = 0`.
      wrong_approach: "Special-case zeros with complex logic"
      correct_approach: "Let the max/min comparison handle zeros naturally"

    - title: Wrong Swap Timing
      description: |
        Some implementations swap `current_max` and `current_min` *after* the multiplication, which is incorrect.

        The swap must happen **before** computing the new values because we're preparing for how the negative will affect the *previous* max and min.

        ```python
        # Wrong: swap after
        current_max = max(num, current_max * num)
        current_min = min(num, current_min * num)
        if num < 0:
            current_max, current_min = current_min, current_max  # Too late!
        ```
      wrong_approach: "Swap after computing new max/min"
      correct_approach: "Swap before computing when num is negative"

  key_takeaways:
    - "**Track extremes in both directions**: When operations can flip signs, track both maximum and minimum values"
    - "**Negative numbers need special handling**: In products, a very negative value can become the maximum after another negative"
    - "**Extension of Kadane's algorithm**: The `max(current, current * num)` pattern extends to products with the min-tracking addition"
    - "**Foundation for similar problems**: This dual-tracking technique applies to other problems where values can flip between positive and negative"

  time_complexity: "O(n). We traverse the array exactly once, performing constant-time operations at each element."
  space_complexity: "O(1). We only use three variables (`max_product`, `current_max`, `current_min`) regardless of input size."

solutions:
  - approach_name: Dynamic Programming with Min/Max Tracking
    is_optimal: true
    code: |
      def max_product(nums: list[int]) -> int:
          if not nums:
              return 0

          # Initialise with first element
          max_product = nums[0]
          current_max = nums[0]
          current_min = nums[0]

          for i in range(1, len(nums)):
              num = nums[i]

              # If negative, max becomes min and min becomes max after multiplication
              if num < 0:
                  current_max, current_min = current_min, current_max

              # Either start fresh with num, or extend the previous subarray
              current_max = max(num, current_max * num)
              current_min = min(num, current_min * num)

              # Update global maximum
              max_product = max(max_product, current_max)

          return max_product
    explanation: |
      **Time Complexity:** O(n) — Single pass through the array.

      **Space Complexity:** O(1) — Only three variables used.

      The key insight is swapping max and min when we encounter a negative number. This handles the sign-flip elegantly: multiplying a large negative (previous min) by a negative gives a large positive (new max).

  - approach_name: Dynamic Programming (Explicit Candidates)
    is_optimal: true
    code: |
      def max_product(nums: list[int]) -> int:
          if not nums:
              return 0

          max_product = nums[0]
          current_max = nums[0]
          current_min = nums[0]

          for i in range(1, len(nums)):
              num = nums[i]

              # Consider all three candidates for new max and min
              candidates = (num, current_max * num, current_min * num)

              current_max = max(candidates)
              current_min = min(candidates)

              max_product = max(max_product, current_max)

          return max_product
    explanation: |
      **Time Complexity:** O(n) — Single pass through the array.

      **Space Complexity:** O(1) — Only three variables used.

      This version makes the logic more explicit by considering all three candidates at once: the number itself (starting fresh), extending with previous max, or extending with previous min. The swap is implicit in the candidate selection.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def max_product(nums: list[int]) -> int:
          n = len(nums)
          max_product = nums[0]

          # Try every possible subarray
          for i in range(n):
              product = 1
              for j in range(i, n):
                  product *= nums[j]
                  max_product = max(max_product, product)

          return max_product
    explanation: |
      **Time Complexity:** O(n^2) — Nested loops checking all subarrays.

      **Space Complexity:** O(1) — Only tracking the current product and maximum.

      This approach checks every contiguous subarray by fixing a start index and extending to all possible end indices. While correct, it's too slow for large inputs. Included to show why the DP approach is necessary.
