DSA sheet · Arrays · Kadane's Algorithm pattern
Maximum Product Subarray
The second question of the Kadane's section. It looks like Maximum Subarray with × instead of +, but the sign of a product can flip: one negative makes a product negative, a second negative makes it positive again. The teacher writes the brute force first (all subarrays, keep the biggest product), then checks her four patterns and finds that two of them could work. She explains why the plain Kadane's rule "drop it when it goes negative" breaks for products, says the Kadane's fix is a small "state DP" that she will cover in a later video, and solves it here with a prefix and suffix product scan, including how to handle zeros.
Why it matters: a very negative product today can become the biggest product tomorrow, as soon as one more negative number arrives. Learning to keep that in mind (either by scanning from both ends, or by tracking the smallest product too) is the key idea here.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting (subarrays, Kadane's, signs and zeros from scratch)
- Part A · Brute force: multiply out every subarray
- Part B · Optimal (the teacher's way): prefix and suffix products
- Part C · The Kadane's way: track the max AND the min (the "state DP" she promises)
- Part D · Revision page
Part 0 · Before starting
What is a subarray?
A subarray is a piece of the array whose elements sit next to each other (contiguous): a start index, an end index, and everything in between, with nothing skipped. Its product is all its elements multiplied together.
The brute force over all subarrays
Fix a start i, then move an end j from i to the last index, multiplying one more element into a running product each time. That visits all n·(n+1)/2 subarrays, so it is O(n²), and it redoes nearly the same multiplications for every start.
Kadane's idea for sums (from the previous problem)
For the maximum sum, we walk once and keep a running sum. At every index the best subarray ending there either extends the previous one or starts fresh. Extending is only worth it if the past total is positive, so the rule is: add the number, record the best, and if the running sum is negative, reset it to 0. That's safe because a stretch with a negative sum can only lower any subarray that includes it, so the best subarray never begins with one. For an all-negative array, the best must be the largest single number, so the best starts at nums[0] (never 0) and is recorded before each reset.
Why products are different: the sign flips
- positive × positive = positive · positive × negative = negative · negative × negative = positive.
- So a product that is very negative right now is not "bad". If another negative comes later, it turns into a very large positive.
- That's why "throw it away when it goes negative" (the sum rule) is wrong for products. The teacher's example: [−2, 3, 4, −1]. Dropping −2 at once, then 3 × 4 = 12, then dropping −1, gives 12. But all four together give (−2)·3·4·(−1) = 24.
- When a negative appears, you don't yet know whether keeping it will pay off. That depends on whether another negative comes later. So we keep two possibilities alive. There are two standard ways:
- Track both the max and the min product ending here (Kadane's with two states). The min is the "most negative" one, which a later negative can flip into the max. The teacher calls this a state DP (keep both "took it" and "didn't take it" alive) and leaves it for a later video. Part C shows it.
- Prefix and suffix scan: multiply from the left and from the right, and take the best product seen in either scan. This is what the teacher teaches in this video (Part B).
Zeros reset everything
Anything times 0 is 0, and it stays 0 forever after. A subarray that crosses a zero has product 0, so a zero splits the array into separate pieces. Once a running product hits 0, we record that 0 (it can be the answer, e.g. [−2, 0, −1] → 0) and then restart the product at 1 on the other side.
How this relates to sliding window, and why a plain window fails
Sliding window grows the window with right and shrinks it with left. That needs a monotonic rule. For example, with positive numbers ≥ 1, a product only grows when you add an element and only drops when you remove one. Then "grow until invalid, then shrink" is safe (that's how Subarray Product Less Than K works).
Here negatives break that. Adding −2 can turn a huge positive into a huge negative, and adding another −1 turns it back. There is no "valid / invalid" condition to grow and shrink by, and moving left one step can raise or lower the product depending on the sign. The teacher rules sliding window out for exactly this reason: it needs all numbers positive.
The teacher's 4 array patterns, checked for this problem
| pattern | when it fits | here? |
|---|---|---|
| Two pointers | sorted array, and only the two pointed elements matter, nothing in between | no, a subarray needs everything in between |
| Sliding window | subarray sum or product, but only with positive numbers | no, negatives allowed |
| Prefix sum (here: prefix/suffix products) | subarray sums or products, negatives allowed | yes (Part B) |
| Kadane's | maximum with negatives, by restarting the running value | yes, with two states (Part C) |
Part A · Brute force: multiply out every subarray
LeetCode 152
1The question in simple words
You get an integer array nums. Find the (non-empty) subarray whose product is the largest, and return that product.
2What the constraints tell us
- The answer fits in a 32-bit integer (LeetCode promises this). The teacher's point: multiplying many numbers can easily get huge, and normally you'd reach for a
long. Here the tests are built so anintis enough. (In Python ints never overflow, so we don't worry about it at all.) - 1 ≤ n ≤ 2·10⁴. O(n²) is about 4·10⁸, past the ~10⁸ comfort line. It will be very slow or TLE, so we should optimise.
- −10 ≤ nums[i] ≤ 10. Numbers can be negative (and can be 0). That decides the pattern: one negative makes the product negative, two negatives make it positive again.
3Intuition: list every subarray, keep the biggest product
If you know how to generate all subarrays, the problem is easy. Stand at start i, let j walk to the end, and multiply nums[j] into a running product. Each step gives the product of [i..j]. Compare it with the best so far.
4Building the logic from the example
nums = [2, 3, −2, 4]:
- i = 0: [2] → 2 · [2, 3] → 6 · [2, 3, −2] → −12 · [2, 3, −2, 4] → −48.
- i = 1: [3] → 3 · [3, −2] → −6 · [3, −2, 4] → −24.
- i = 2: [−2] → −2 · [−2, 4] → −8.
- i = 3: [4] → 4.
The biggest is 6.
The running product starts at 1, not 0
For a sum we start at 0, because adding 0 changes nothing. For a product, starting at 0 would make every product 0. The "do nothing" value for multiplication is 1, so prod = 1 for each new start.
What should the best answer start as?
The teacher weighs 0, −1, −∞ and −10:
- 0 or −1 are not safe. If the array is just [−10], the answer is −10, but a start of 0 or −1 would never be replaced.
- −∞ is safe: any real product beats it.
- −10 is also safe, and more precise: the smallest allowed number is −10, so the answer (which is at least as big as the largest single element) can never be below −10. With [−10] alone, −10 is exactly right. With [−10, −10], the product 100 simply replaces it.
→ No. We only need the start to be ≤ the final answer, not ≤ every product. Every single element is itself a subarray, so the answer is at least the largest element, which is ≥ −10. Starting at nums[0] would also work.
→ Because the best subarray from start i can end anywhere. From i = 0 the products went 2, 6, −12, −48. The best (6) was in the middle.
5Approach steps
max_prod = -10(or −∞).- For each start
i:prod = 1. - For each end
jfromi:prod *= nums[j], thenmax_prod = max(max_prod, prod). - Return
max_prod.
6Code (Python)
class Solution:
def maxProduct(self, nums):
n = len(nums)
max_prod = -10 # smallest possible answer (or float('-inf'))
for i in range(n): # start of the subarray
prod = 1 # 1, not 0: the neutral value for *
for j in range(i, n): # end of the subarray
prod *= nums[j] # prod = product of nums[i..j]
max_prod = max(max_prod, prod)
return max_prod7Code line by line
| line | what it means |
|---|---|
| max_prod = -10 | A safe lower bound for the answer (Doubt 1). −∞ works too. |
| for i in range(n): | Every index gets a turn as the start. |
| prod = 1 | A fresh product for this start. Must be 1: starting at 0 would make everything 0. |
| for j in range(i, n): prod *= nums[j] | Grow the subarray by one element, and multiply it in. |
| max_prod = max(max_prod, prod) | Keep the biggest product seen so far. |
| return max_prod | Every subarray has been tried. |
8Dry run (hand table)
| start i | running products as j moves | best from this start | max_prod after |
|---|---|---|---|
| 0 | 2, 6, −12, −48 | 6 | 6 |
| 1 | 3, −6, −24 | 3 | 6 |
| 2 | −2, −8 | −2 | 6 |
| 3 | 4 | 4 | 6 |
Final answer: 6 ✓.
9Complexity & remember
- Time O(n²): the inner loop runs about n, then n − 1, then n − 2, … times, so about n²/2 in total. For n = 2·10⁴ that's ~2·10⁸ multiplications. The teacher's submission was accepted but very slow. (In Python this is likely to TLE.)
- Space O(1).
prod = 1 per start, compare after every multiplication. Start the answer at −10 / −∞, never 0.Part B · Optimal (the teacher's way): prefix and suffix products
LeetCode 152
1The question again, with the new goal
Same question, in O(n): one pass, no restarting from every index.
2Why not just copy Kadane's from the sum problem?
The teacher tries the plain Kadane's rule on [−2, 3, 4, −1]:
- −2 is negative → drop it. Start again at 3.
- 3 × 4 = 12.
- × (−1) = −12, negative → drop it. Answer 12?
But taking all four gives (−2)·3·4·(−1) = 24, which is far bigger. The two negatives cancel. And if the last −1 weren't there, keeping −2 would have been a mistake ([−2, 3, 4] = −24 < 12). So when you meet a negative, you can't decide yet whether to keep it. You'd need to carry both options forward ("took it" and "didn't take it"). That is the state-DP version of Kadane's, which she defers. (It's written out in Part C.) Here she uses the other pattern that fits: prefix and suffix.
3Intuition: multiply from the left, and from the right
- Prefix scan: walk left → right, multiplying as you go. After each step you have the product of a subarray that starts at the left edge. Record the best.
- Suffix scan: walk right → left the same way. Now each product is a subarray that ends at the right edge. Record the best.
- The answer is the best product seen in either scan.
Why should the best subarray always touch one of the edges? Because with whole numbers, multiplying in more non-zero elements never makes the size (absolute value) of the product smaller. Only the sign can go wrong. If the count of negatives is even, the whole stretch is positive and is the biggest. If it's odd, we must leave out one negative, together with everything on one side of it. The best way is to cut off the first negative and everything before it (that's a suffix), or the last negative and everything after it (that's a prefix). One of the two scans finds it.
4Building the logic from examples
Example 1: [2, 3, −2, 4]: the prefix scan alone works
Left → right: 2 (best 2), 6 (best 6), −12, −48. Best = 6. That is the answer.
Example 2: add one more number, [2, 3, −2, 4, 2]: the prefix scan misses it
Left → right: 2, 6, −12, −48, −96. Best from the left is still 6. But [4, 2] = 8 is bigger. The prefix scan never sees it, because it is stuck carrying the −2. So the teacher runs a second scan from the right end: 2, 2·4 = 8, then × (−2) = −16, × 3 = −48, × 2 = −96. Best from the right = 8. Answer = max(6, 8) = 8 ✓.
Example 3: a zero in the middle, [2, 3, −2, 4, 0, 2, 4]
Left → right: 2, 6, −12, −48, then × 0 = 0. From here every product would be 0 forever (0 × 2, 0 × 4, …). We don't want to carry the zero. So whenever the product becomes 0, we start again from 1: 1 × 2 = 2, then × 4 = 8. The suffix side does the same: from the right, 4, 8, then × 0 = 0 → restart at 1 → 1 × 4 = 4, and so on. Both scans find 8.
→ Yes, and it must be. In the teacher's code the reset happens at the start of the next step, so in the step where we multiply by 0, the product 0 is compared with the answer first. That matters for arrays like [−2, 0, −1], where every other product is negative and the answer is 0.
→ The values are integers, and between zeros none of them is 0, so each has size at least 1. Multiplying by something of size ≥ 1 can't shrink the size. (With fractions like 0.5 this argument would break, but the input is integers.)
→ Both scans record −3 as a product, and they also record the 0s next to it. The answer is max(0, −3) = 0, which is correct. For the array [−3] alone, the answer is −3, which is recorded in the first step.
One loop instead of two: the index n − i − 1
We could write two loops (left → right, then right → left), which is O(2n). The teacher merges them into one: at step i, the prefix multiplies nums[i] and the suffix multiplies the element at the mirror position from the end. With n = 4: when i = 0 the suffix needs index 3; i = 1 → 2; i = 2 → 1; i = 3 → 0. The formula is n − i − 1: 4 − 0 − 1 = 3, 4 − 1 − 1 = 2, and so on.
5Approach steps
prefix = 1,suffix = 1(1, not 0, because we multiply),ans = −∞(or −10).- For each
ifrom 0 to n − 1: - If
prefix == 0, set it back to 1. Same forsuffix. (A zero was just passed.) prefix *= nums[i],suffix *= nums[n − i − 1].ans = max(ans, prefix, suffix).- Return
ans.
6Code (Python)
class Solution:
def maxProduct(self, nums):
n = len(nums)
prefix = 1 # product from the left edge (or since the last 0)
suffix = 1 # product from the right edge (or since the last 0)
ans = float('-inf') # -10 also works
for i in range(n):
if prefix == 0: # we crossed a zero: start again
prefix = 1
if suffix == 0:
suffix = 1
prefix *= nums[i] # left -> right
suffix *= nums[n - i - 1] # right -> left, at the same time
ans = max(ans, prefix, suffix)
return ans7Code line by line
| line | what it means |
|---|---|
| prefix = 1 suffix = 1 | Empty products. 1 changes nothing when multiplied, 0 would wipe everything out. |
| ans = float('-inf') | Anything real beats it. (−10 is fine too, as in Part A.) |
| if prefix == 0: prefix = 1 | The last element we multiplied was a 0. Its 0 was already recorded in ans last step. Now start a fresh product on the far side of that zero. |
| if suffix == 0: suffix = 1 | Same for the right-to-left scan. |
| prefix *= nums[i] | Extend the left-anchored product by one element. |
| suffix *= nums[n - i - 1] | Extend the right-anchored product by one element, moving in from the end. |
| ans = max(ans, prefix, suffix) | Is either of the two products the best seen so far? |
| return ans | The best product from either direction. |
8Dry run
Run 1: nums = [2, 3, −2, 4, 2] (n = 5)
| i | nums[i] | prefix after | n−i−1 | nums[n−i−1] | suffix after | decision | best so far |
|---|---|---|---|---|---|---|---|
| 0 | 2 | 2 | 4 | 2 | 2 | extend both | 2 |
| 1 | 3 | 6 | 3 | 4 | 8 | extend both | 8 |
| 2 | −2 | −12 | 2 | −2 | −16 | extend both (a negative is not a reason to restart) | 8 |
| 3 | 4 | −48 | 1 | 3 | −48 | extend both | 8 |
| 4 | 2 | −96 | 0 | 2 | −96 | extend both | 8 |
Answer 8 ✓ ([4, 2]). The prefix alone would have stopped at 6.
Run 2: nums = [2, 3, −2, 4, 0, 2, 4] (n = 7), with a zero
| i | nums[i] | prefix (reset?) | nums[n−i−1] | suffix (reset?) | decision | best so far |
|---|---|---|---|---|---|---|
| 0 | 2 | 2 | 4 | 4 | extend both | 4 |
| 1 | 3 | 6 | 2 | 8 | extend both | 8 |
| 2 | −2 | −12 | 0 | 0 | suffix hits the zero (0 is recorded) | 8 |
| 3 | 4 | −48 | 4 | reset → 1 × 4 = 4 | suffix restarts past the zero | 8 |
| 4 | 0 | 0 | −2 | −8 | prefix hits the zero | 8 |
| 5 | 2 | reset → 1 × 2 = 2 | 3 | −24 | prefix restarts past the zero | 8 |
| 6 | 4 | 8 | 2 | −48 | extend both | 8 |
The prefix scan at its key moments (yellow = the current prefix product, grey = left behind by a zero reset):
Answer 8 ✓.
9Complexity & remember
- Time O(n): one loop that does two multiplications per step. Two separate loops would be O(2n), still linear, but one loop is neater.
- Space O(1): three variables.
She submitted it and it beat about 90%: "the fastest solution" for this problem.
ans = max(ans, prefix, suffix). Suffix index: n − i − 1.Part C · The Kadane's way: track the max AND the min
This is the "state DP" version of Kadane's that the teacher mentions and leaves for a later video. It is not coded in this video. It is written here so the page is complete, and you'll meet it again.
1The question
The same as Part A. We want one pass, in Kadane's style.
2Constraints
The same as Part A. Negatives and zeros are allowed, and n is up to 2·10⁴, so O(n) is the goal.
3Intuition: keep two runs alive
For sums, one number was enough: the best sum ending here. For products, we keep two:
cur_max: the largest product of a subarray ending at this index;cur_min: the smallest (most negative) product of a subarray ending at this index.
When the next number x is negative, the roles swap: the smallest product times x becomes the largest, and the largest times x becomes the smallest. That's the teacher's "took it / didn't take it" idea. The very negative run is kept as cur_min in case a later negative flips it.
4Building the logic
At each index, the best subarray ending here is one of three things: x alone (start fresh), cur_max × x (extend the biggest), or cur_min × x (extend the smallest, which wins when x < 0). So:
new_max = max(x, cur_max·x, cur_min·x)new_min = min(x, cur_max·x, cur_min·x)
A common shortcut: if x < 0, swap cur_max and cur_min first. Then cur_max = max(x, cur_max·x) and cur_min = min(x, cur_min·x).
→ With x = 0, both become max/min(0, 0, 0) = 0. On the next number y,
max(y, 0·y, 0·y) is just "y vs 0", so the "start fresh at y" option takes over. Zeros reset things on their own. No special if is needed.→ If you update cur_max first and then use the new cur_max to compute cur_min, you mix two different steps. Compute both from the old pair (Python's
a, b = …, … does this), or use the swap trick.5Approach steps
cur_max = cur_min = ans = nums[0].- For each next number x: if x < 0, swap cur_max and cur_min.
cur_max = max(x, cur_max·x),cur_min = min(x, cur_min·x).ans = max(ans, cur_max). Return ans at the end.
6Code (Python)
class Solution:
def maxProduct(self, nums):
cur_max = cur_min = ans = nums[0]
for x in nums[1:]:
if x < 0: # a negative flips big and small
cur_max, cur_min = cur_min, cur_max
cur_max = max(x, cur_max * x) # start fresh, or extend
cur_min = min(x, cur_min * x)
ans = max(ans, cur_max)
return ans7Code line by line
| line | what it means |
|---|---|
| cur_max = cur_min = ans = nums[0] | The only subarray ending at index 0 is [nums[0]]. |
| if x < 0: swap | Times a negative, the old smallest becomes the new largest candidate, and the old largest becomes the new smallest. |
| cur_max = max(x, cur_max * x) | The best product ending here: x alone, or extend. |
| cur_min = min(x, cur_min * x) | The most negative product ending here, kept for a future flip. |
| ans = max(ans, cur_max) | Record the best seen anywhere. |
8Dry run on the teacher's [−2, 3, 4, −1]
| i | nums[i] | swap? | cur max | cur min | decision | best so far |
|---|---|---|---|---|---|---|
| 0 | −2 | – | −2 | −2 | start | −2 |
| 1 | 3 | no | max(3, −6) = 3 | min(3, −6) = −6 | max restarts at 3 · min keeps the −2 alive (−6) | 3 |
| 2 | 4 | no | max(4, 12) = 12 | min(4, −24) = −24 | both extend | 12 |
| 3 | −1 | yes → max −24, min 12 | max(−1, 24) = 24 | min(−1, −12) = −12 | the kept −24 flips into 24 | 24 |
Answer 24 ✓, the same answer the prefix scan gives (its prefix after four steps is 24).
9Complexity & remember
- Time O(n), Space O(1), the same as Part B.
Part D · Revision page
| Brute force | Prefix & suffix (video) | Max/min Kadane's | |
|---|---|---|---|
| idea | every subarray, keep the max product | scan from both ends, reset to 1 after a zero | keep the largest and smallest product ending here |
| handles negatives by | trying everything | one of the two scans drops the bad negative's side | swapping max and min on a negative |
| handles zeros by | nothing special | if prefix == 0: prefix = 1 | automatic (max(x, 0) restarts) |
| time / space | O(n²) / O(1) | O(n) / O(1) | O(n) / O(1) |
| Maximum Subarray (sum) | Maximum Product Subarray | |
|---|---|---|
| neutral start | sum = 0 | product = 1 |
| restart when | the running sum is negative | the running product is 0 (a negative may flip later!) |
| states kept | one | two (max & min), or two scans |
2. Scan products from the left and from the right. The best is in one of them.
3. Products start at 1. After a zero, restart at 1 (the 0 itself is already recorded).
4. Suffix index =
n − i − 1, so both scans fit in one loop.5. The Kadane's version keeps max and min, and swaps them on a negative.
✗ starting the answer at 0 or −1 (fails on [−10] or [−3])
✗ using the sum-Kadane's rule "drop it when negative" (misses 24 in [−2, 3, 4, −1])
✗ only scanning from the left (misses 8 in [2, 3, −2, 4, 2])
✗ carrying a 0 forward (everything after becomes 0)
✗ resetting before recording the 0 (fails on [−2, 0, −1])
s = Solution() print(s.maxProduct([2, 3, -2, 4])) # 6 print(s.maxProduct([-2, 0, -1])) # 0 print(s.maxProduct([2, 3, -2, 4, 2])) # 8 (needs the suffix scan) print(s.maxProduct([-2, 3, 4, -1])) # 24 (two negatives cancel) print(s.maxProduct([2, 3, -2, 4, 0, 2, 4])) # 8 (zero reset) print(s.maxProduct([-3])) # -3 (single element) print(s.maxProduct([-2, -3, -4])) # 12 (odd count of negatives)
Based on this video: Maximum Product Subarray | Prefix & Suffix