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 · 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.

index0123
nums23-24[2, 3] is a subarray, product 6
nums23-24[2, 4] is not (3 and −2 are skipped)

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

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

patternwhen it fitshere?
Two pointerssorted array, and only the two pointed elements matter, nothing in betweenno, a subarray needs everything in between
Sliding windowsubarray sum or product, but only with positive numbersno, negatives allowed
Prefix sum (here: prefix/suffix products)subarray sums or products, negatives allowedyes (Part B)
Kadane'smaximum with negatives, by restarting the running valueyes, 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.

nums23-24answer 6, from [2, 3]
nums-20-1answer 0: [−2, −1] is not a subarray (0 sits between them)

2What the constraints tell us

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]:

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:

Doubt 1: products like −10 × 10 = −100 are below −10. Isn't −10 too high a start then?
→ 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.
Doubt 2: why compare after every multiplication?
→ 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

  1. max_prod = -10 (or −∞).
  2. For each start i: prod = 1.
  3. For each end j from i: prod *= nums[j], then max_prod = max(max_prod, prod).
  4. Return max_prod.

6Code (Python)

Brute force: O(n²), very slow for n = 2·10⁴
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_prod

7Code line by line

linewhat it means
max_prod = -10A safe lower bound for the answer (Doubt 1). −∞ works too.
for i in range(n):Every index gets a turn as the start.
prod = 1A 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_prodEvery subarray has been tried.

8Dry run (hand table)

start irunning products as j movesbest from this startmax_prod after
02, 6, −12, −4866
13, −6, −2436
2−2, −8−26
3446

Final answer: 6 ✓.

9Complexity & remember

Remember the brute forceOuter loop = start, inner loop = end, 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]:

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

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.

Doubt 1: we restart after a zero. Does the 0 itself ever get recorded as an answer?
→ 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.
Doubt 2: why does "more numbers multiplied → bigger size" hold?
→ 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.)
Doubt 3: a stretch between zeros that is just one negative, like [−3] in [0, −3, 0]?
→ 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

  1. prefix = 1, suffix = 1 (1, not 0, because we multiply), ans = −∞ (or −10).
  2. For each i from 0 to n − 1:
  3. If prefix == 0, set it back to 1. Same for suffix. (A zero was just passed.)
  4. prefix *= nums[i], suffix *= nums[n − i − 1].
  5. ans = max(ans, prefix, suffix).
  6. Return ans.

6Code (Python)

Prefix and suffix products: O(n)
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 ans

7Code line by line

linewhat it means
prefix = 1 suffix = 1Empty 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 = 1The 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 = 1Same 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 ansThe best product from either direction.

8Dry run

Run 1: nums = [2, 3, −2, 4, 2] (n = 5)

inums[i]prefix aftern−i−1nums[n−i−1]suffix afterdecisionbest so far
022422extend both2
136348extend both8
2−2−122−2−16extend both (a negative is not a reason to restart)8
34−4813−48extend both8
42−9602−96extend both8

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

inums[i]prefix (reset?)nums[n−i−1]suffix (reset?)decisionbest so far
02244extend both4
13628extend both8
2−2−1200suffix hits the zero (0 is recorded)8
34−484reset → 1 × 4 = 4suffix restarts past the zero8
400−2−8prefix hits the zero8
52reset → 1 × 2 = 23−24prefix restarts past the zero8
6482−48extend both8

The prefix scan at its key moments (yellow = the current prefix product, grey = left behind by a zero reset):

i = 123-24024prefix 6
LR     
i = 423-24024prefix hits 0 → record 0, reset next step
L   R  
i = 623-24024fresh product 2 × 4 = 8
     LR

Answer 8 ✓.

9Complexity & remember

She submitted it and it beat about 90%: "the fastest solution" for this problem.

Remember prefix & suffix Two running products, one from each end, both starting at 1. If one became 0, reset it to 1 before multiplying. 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:

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:

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).

Doubt 1: what happens at a zero?
→ 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.
Doubt 2: why must the two new values be computed from the old ones at the same time?
→ 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

  1. cur_max = cur_min = ans = nums[0].
  2. For each next number x: if x < 0, swap cur_max and cur_min.
  3. cur_max = max(x, cur_max·x), cur_min = min(x, cur_min·x).
  4. ans = max(ans, cur_max). Return ans at the end.

6Code (Python)

Kadane's with max and min: O(n)
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 ans

7Code line by line

linewhat it means
cur_max = cur_min = ans = nums[0]The only subarray ending at index 0 is [nums[0]].
if x < 0: swapTimes 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]

inums[i]swap?cur maxcur mindecisionbest so far
0−2–−2−2start−2
13nomax(3, −6) = 3min(3, −6) = −6max restarts at 3 · min keeps the −2 alive (−6)3
24nomax(4, 12) = 12min(4, −24) = −24both extend12
3−1yes → max −24, min 12max(−1, 24) = 24min(−1, −12) = −12the kept −24 flips into 2424
i = 2-234-1cur_min run = −24 (plain Kadane's would have dropped −2)
L R 
i = 3-234-1× −1 → 24
L  R

Answer 24 ✓, the same answer the prefix scan gives (its prefix after four steps is 24).

9Complexity & remember

Remember max/min Kadane'sKeep the biggest AND smallest product ending here. A negative swaps them. Each step: start fresh at x, or extend.

Part D · Revision page

Brute forcePrefix & suffix (video)Max/min Kadane's
ideaevery subarray, keep the max productscan from both ends, reset to 1 after a zerokeep the largest and smallest product ending here
handles negatives bytrying everythingone of the two scans drops the bad negative's sideswapping max and min on a negative
handles zeros bynothing specialif prefix == 0: prefix = 1automatic (max(x, 0) restarts)
time / spaceO(n²) / O(1)O(n) / O(1)O(n) / O(1)
Maximum Subarray (sum)Maximum Product Subarray
neutral startsum = 0product = 1
restart whenthe running sum is negativethe running product is 0 (a negative may flip later!)
states keptonetwo (max & min), or two scans
If you remember only 5 lines 1. One negative flips the sign, and two negatives flip it back. So never throw away a negative product.
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.
Mistakes to avoid ✗ starting the product at 0
✗ 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])
test it yourself (paste under any Solution above)
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