241 lines
9.8 KiB
YAML
241 lines
9.8 KiB
YAML
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:
|
|
- slug: dynamic-programming
|
|
is_optimal: true
|
|
|
|
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.
|