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)   **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   **Step 3: Return the result** - Return `max_product` — the largest product subarray found   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.