Maximum Product Subarray Explained — Kadane's Twist, Edge Cases & Interview Traps
Most people encounter the Maximum Product Subarray problem in a coding interview and think: 'I know the maximum sum version — how different can this be?' Very different, it turns out. Multiplication has a superpower addition doesn't: two negatives make a positive. That single fact blows up the naive approach and forces you to think in a completely new way. This isn't just an interview puzzle — understanding it sharpens your instinct for any problem where values interact non-linearly, like financial risk modeling, signal processing chains, or evaluating compound growth rates across time windows.
The problem itself is deceptively simple to state: given an integer array, find the contiguous subarray that produces the largest product. But unlike the maximum sum version (solved elegantly by Kadane's Algorithm), you can't just throw away a subarray the moment it looks bad. A deeply negative running product might be one more negative number away from becoming the best product you've ever seen. That's the problem. Your current 'worst' could become tomorrow's 'best'.
By the end of this article you'll understand exactly why you need to track both a maximum AND a minimum running product simultaneously, how to handle the zero-element trap that silently resets everything, and how to write a clean O(n) solution that will impress any interviewer. You'll also see the common mistakes that cause candidates to fail this question even after they think they've solved it.
Why Kadane's Sum Algorithm Breaks Down for Products
Kadane's Algorithm for maximum sum works on a simple rule: if your running total goes negative, cut your losses and restart from the current element. Negative running sum? Useless. Throw it away.
Products don't follow that rule. A running product of -1000 is not useless. If the next element is -5, your product jumps to +5000 — instantly the best result you've ever seen. You can never safely discard a negative running product the way you discard a negative running sum.
This forces a key insight: at every position in the array, you need to know two things simultaneously — the maximum product ending here AND the minimum product ending here. The minimum matters because multiplying it by a negative number flips it into the maximum.
Think of it like tracking two runners in a race where the track randomly reverses direction. You keep tabs on whoever is furthest ahead AND furthest behind, because the next reversal could swap their positions entirely. That mental model is the algorithm.
public class MaxProductSubarrayBruteForce { /** * Brute-force O(n^2) approach — checks every possible subarray. * We show this FIRST so you feel the pain before seeing the fix. */ public static int findMaxProductBruteForce(int[] prices) { int n = prices.length; if (n == 0) return 0; int overallMax = prices[0]; // start with first element, not Integer.MIN_VALUE // because all elements could be negative for (int start = 0; start < n; start++) { int runningProduct = 1; for (int end = start; end < n; end++) { runningProduct *= prices[end]; // extend subarray one step right // update global max after each extension overallMax = Math.max(overallMax, runningProduct); } } return overallMax; } public static void main(String[] args) { int[] stockMultipliers = {2, 3, -2, 4}; System.out.println("Max product (brute force): " + findMaxProductBruteForce(stockMultipliers)); // Subarrays: [2]=2, [2,3]=6, [2,3,-2]=-12, [2,3,-2,4]=-48 // [3]=3, [3,-2]=-6, [3,-2,4]=-24 // [-2]=-2, [-2,4]=-8 // [4]=4 // Winner: [2, 3] = 6 int[] withRescuingNegatives = {-2, -3, 4}; System.out.println("Max product (brute force): " + findMaxProductBruteForce(withRescuingNegatives)); // [-2,-3]=6, [-2,-3,4]=24 <-- two negatives rescue each other! // Winner: [-2, -3, 4] = 24 } }
Max product (brute force): 24
The O(n) Solution — Tracking Both Max and Min Simultaneously
Here's the core idea laid out plainly: at each position, you compute the maximum and minimum products that end at that position. You need both because a negative number times the current minimum (a large negative) could produce the new maximum, and vice versa.
At each step, your new maximum is the largest of three candidates: the current element alone (starting fresh), the previous maximum times the current element, or the previous minimum times the current element (the rescue scenario). Your new minimum is the smallest of those same three candidates.
The 'current element alone' candidate is crucial — it represents cutting all ties with history and starting a fresh subarray from this position. This is where zeros play their role: any product times zero is zero, which resets both trackers and forces a fresh start from the next element.
This runs in O(n) time with O(1) space — you never need the full array history, just the two values from the previous step. It's one of those algorithms that looks like magic until you see the picture, and then it's obvious.
public class MaxProductSubarrayOptimal { /** * O(n) time, O(1) space solution. * The key insight: track BOTH the max and min product ending at each index. * A negative number flips min into max and max into min — so we need both. */ public static int findMaxProduct(int[] numbers) { if (numbers == null || numbers.length == 0) { throw new IllegalArgumentException("Array must not be null or empty"); } // Initialize all three with the first element int maxProductSoFar = numbers[0]; // the answer we'll return int maxEndingHere = numbers[0]; // best product of subarray ending at current index int minEndingHere = numbers[0]; // worst product of subarray ending at current index for (int i = 1; i < numbers.length; i++) { int currentElement = numbers[i]; // When we multiply by currentElement, max and min might swap. // We must compute both candidates BEFORE overwriting either variable. // That's why we save maxEndingHere in a temp variable first. int previousMax = maxEndingHere; // New maxEndingHere = best of: // 1) start fresh from currentElement // 2) extend the previous max subarray (positive * positive = bigger positive) // 3) extend the previous min subarray (negative * negative = positive) maxEndingHere = Math.max( currentElement, Math.max(previousMax * currentElement, minEndingHere * currentElement) ); // New minEndingHere = worst of the same three candidates // (we want this because a future negative might flip it to a huge positive) minEndingHere = Math.min( currentElement, Math.min(previousMax * currentElement, minEndingHere * currentElement) ); // Update global answer — max found anywhere in the array, not just at end maxProductSoFar = Math.max(maxProductSoFar, maxEndingHere); } return maxProductSoFar; } public static void main(String[] args) { // Test 1: Classic case — best subarray is [2, 3] int[] basicCase = {2, 3, -2, 4}; System.out.println("Test 1 — [2, 3, -2, 4]: " + findMaxProduct(basicCase)); // Test 2: Two negatives rescue each other — best is [-2, -3, 4] = 24 int[] twoNegatives = {-2, -3, 4}; System.out.println("Test 2 — [-2, -3, 4]: " + findMaxProduct(twoNegatives)); // Test 3: Zero resets everything — best is [5, 6] = 30 int[] withZero = {5, 6, 0, 3, 2}; System.out.println("Test 3 — [5, 6, 0, 3, 2]: " + findMaxProduct(withZero)); // Test 4: All negatives — best is a single element [-1] int[] allNegatives = {-4, -3, -2}; System.out.println("Test 4 — [-4, -3, -2]: " + findMaxProduct(allNegatives)); // [-4,-3]=12, but 12*-2=-24, so best full run is [-4,-3]=12 // Wait — let's think: [-4]=−4, [-3]=−3, [-2]=−2, [-4,-3]=12, [-3,-2]=6, [-4,-3,-2]=−24 // Winner: 12 ✓ // Test 5: Single element int[] singleElement = {-7}; System.out.println("Test 5 — [-7]: " + findMaxProduct(singleElement)); } }
Test 2 — [-2, -3, 4]: 24
Test 3 — [5, 6, 0, 3, 2]: 30
Test 4 — [-4, -3, -2]: 12
Test 5 — [-7]: -7
Walking Through the Algorithm Step-by-Step with a Tricky Example
Abstract explanations only go so far. Let's trace through the array [-2, 3, -4] manually so you can see the max/min flip happening in real time.
We expect the answer to be 24, because -2 × 3 × -4 = 24 (the full array is the winning subarray).
Start: maxEndingHere = -2, minEndingHere = -2, maxProductSoFar = -2.
i=1, element=3: candidates are [3, -2×3=-6, -2×3=-6]. maxEndingHere = max(3,-6,-6) = 3. minEndingHere = min(3,-6,-6) = -6. maxProductSoFar = max(-2, 3) = 3.
i=2, element=-4: save previousMax=3. Candidates: [-4, 3×-4=-12, -6×-4=24]. maxEndingHere = max(-4,-12,24) = 24. minEndingHere = min(-4,-12,24) = -12. maxProductSoFar = max(3, 24) = 24.
The minimum (-6) that we kept alive at i=1 was the seed that produced 24 at i=2. If we had thrown it away, we'd have returned 3.
public class MaxProductTraceVisualizer { /** * Same algorithm as before, but with detailed tracing output * so you can see exactly how max and min evolve at each step. * Use this as a learning/debugging tool. */ public static int findMaxProductWithTrace(int[] numbers) { int maxProductSoFar = numbers[0]; int maxEndingHere = numbers[0]; int minEndingHere = numbers[0]; System.out.printf("%-5s %-8s %-14s %-14s %-16s%n", "i", "num[i]", "maxEndingHere", "minEndingHere", "maxProductSoFar"); System.out.printf("%-5s %-8d %-14d %-14d %-16d%n", "init", numbers[0], maxEndingHere, minEndingHere, maxProductSoFar); for (int i = 1; i < numbers.length; i++) { int currentElement = numbers[i]; int previousMax = maxEndingHere; // snapshot before mutation maxEndingHere = Math.max( currentElement, Math.max(previousMax * currentElement, minEndingHere * currentElement) ); minEndingHere = Math.min( currentElement, Math.min(previousMax * currentElement, minEndingHere * currentElement) ); maxProductSoFar = Math.max(maxProductSoFar, maxEndingHere); System.out.printf("%-5d %-8d %-14d %-14d %-16d%n", i, currentElement, maxEndingHere, minEndingHere, maxProductSoFar); } return maxProductSoFar; } public static void main(String[] args) { System.out.println("=== Tracing [-2, 3, -4] ==="); int[] trickyArray = {-2, 3, -4}; int result = findMaxProductWithTrace(trickyArray); System.out.println("\nFinal Answer: " + result); System.out.println("\n=== Tracing [1, -2, -3, 0, 7, -8, -2] ==="); int[] mixedArray = {1, -2, -3, 0, 7, -8, -2}; int result2 = findMaxProductWithTrace(mixedArray); System.out.println("\nFinal Answer: " + result2); // After 0 resets both to 0, we restart fresh // 7 * -8 * -2 = 112 is the winning subarray } }
i num[i] maxEndingHere minEndingHere maxProductSoFar
init -2 -2 -2 -2
1 3 3 -6 3
2 -4 24 -12 24
Final Answer: 24
=== Tracing [1, -2, -3, 0, 7, -8, -2] ===
i num[i] maxEndingHere minEndingHere maxProductSoFar
init 1 1 1 1
1 -2 -2 -2 1
2 -3 6 -6 6
3 0 0 0 6
4 7 7 7 7
5 -8 -8 -56 7
6 -2 112 -14 112
Final Answer: 112
The Alternate Approach — Forward and Backward Pass
There's a second elegant O(n) solution worth knowing, especially because it offers a completely different mental model and is sometimes easier to explain quickly in an interview.
The observation: in any optimal subarray, an odd number of negatives is what kills you. The only way an odd number of negatives sneaks into your answer is if the first or last element of the subarray is negative. So if you take a forward pass (accumulating product left-to-right) and a backward pass (right-to-left), and reset whenever you hit zero, you're guaranteed to catch every optimal subarray.
Why? Because any subarray with an even count of negatives will be fully captured in at least one direction. Any subarray that avoids a leading or trailing negative will be caught in both. It's a geometric argument, not a dynamic programming one, which makes it a refreshing alternative.
This approach is slightly less intuitive to prove rigorously, but it's often faster to code from memory because you don't need to manage two separate DP variables — just two running products and one loop in each direction.
public class MaxProductTwoPassApproach { /** * Two-pass (forward + backward) approach. * Time: O(n), Space: O(1). * * Core idea: scan left-to-right accumulating products, reset on zero. * Then scan right-to-left doing the same. The max found in either * direction is the answer. * * Why it works: any subarray with an even number of negatives will * show a positive cumulative product in at least one direction. * The zero-reset ensures we never let a zero poison a subarray * on one side of it. */ public static int findMaxProductTwoPass(int[] numbers) { if (numbers == null || numbers.length == 0) { throw new IllegalArgumentException("Array must not be null or empty"); } int n = numbers.length; int maxProduct = Integer.MIN_VALUE; int forwardProduct = 1; int backwardProduct = 1; for (int i = 0; i < n; i++) { forwardProduct *= numbers[i]; // left-to-right backwardProduct *= numbers[n - 1 - i]; // right-to-left (mirror index) // Update global max with both running products maxProduct = Math.max(maxProduct, Math.max(forwardProduct, backwardProduct)); // Reset to 1 (multiplicative identity) when we hit zero // This is equivalent to 'starting fresh' from the next element if (forwardProduct == 0) forwardProduct = 1; if (backwardProduct == 0) backwardProduct = 1; } return maxProduct; } public static void main(String[] args) { int[][] testCases = { {2, 3, -2, 4}, {-2, -3, 4}, {5, 6, 0, 3, 2}, {-4, -3, -2}, {-2, 3, -4}, {1, -2, -3, 0, 7, -8, -2} }; String[] labels = { "[2, 3, -2, 4]", "[-2, -3, 4]", "[5, 6, 0, 3, 2]", "[-4, -3, -2]", "[-2, 3, -4]", "[1, -2, -3, 0, 7, -8, -2]" }; int[] expectedAnswers = {6, 24, 30, 12, 24, 112}; for (int t = 0; t < testCases.length; t++) { int answer = findMaxProductTwoPass(testCases[t]); boolean correct = answer == expectedAnswers[t]; System.out.printf("%-30s => %-5d %s%n", labels[t], answer, correct ? "✓" : "✗ (expected " + expectedAnswers[t] + ")"); } } }
[-2, -3, 4] => 24 ✓
[5, 6, 0, 3, 2] => 30 ✓
[-4, -3, -2] => 12 ✓
[-2, 3, -4] => 24 ✓
[1, -2, -3, 0, 7, -8, -2] => 112 ✓
| Aspect | Min/Max DP Approach | Two-Pass Approach |
|---|---|---|
| Time Complexity | O(n) | O(n) |
| Space Complexity | O(1) | O(1) |
| Number of Passes | 1 pass through array | Logically 2 (done in 1 loop) |
| Variables Needed | maxEndingHere, minEndingHere, maxSoFar | forwardProduct, backwardProduct, maxProduct |
| Handles Zeros | Automatically (math resets to element) | Explicitly (reset to 1 on zero) |
| Handles All Negatives | Yes — keeps worst for future flip | Yes — backward pass catches even-count cases |
| Mental Model | Dynamic programming / state machine | Geometric / directional scan |
| Interview Preference | More commonly expected — shows DP fluency | Impressive as a second solution or alternative |
| Mutation Bug Risk | High — must save previousMax snapshot | Low — two independent variables |
| Intuition Difficulty | Harder to see initially | Easier to explain in words, harder to prove |
🎯 Key Takeaways
- Negatives don't just hurt — they can rescue. A deeply negative running product is one negative element away from becoming the best product in the array. Never discard it.
- You must track both max and min at every step, and you must snapshot maxEndingHere before updating it — the in-place mutation bug is the #1 reason correct-looking solutions produce wrong answers.
- Zero is a hard multiplicative reset. Both running products collapse to zero, and the algorithm restarts naturally from the next element without any special-case code — the 'currentElement alone' candidate handles it.
- There are two clean O(n), O(1) solutions — the min/max DP approach and the two-pass approach. Knowing both and understanding why each works will separate you from 90% of candidates who only memorize one.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Forgetting to snapshot maxEndingHere before updating it — Symptom: wrong answers on arrays with two or more consecutive negatives (e.g., [-2, -3, 4] returns 4 instead of 24) because minEndingHere's calculation reads an already-mutated maxEndingHere — Fix: always store
int previousMax = maxEndingHerebefore any updates and use previousMax in both the max and min calculations. - ✕Mistake 2: Initializing maxProductSoFar to 0 instead of the first element — Symptom: returns 0 for all-negative arrays like [-5, -2, -3] when the correct answer is 6 (i.e., -2 × -3), because the algorithm never finds anything greater than 0 — Fix: initialize maxProductSoFar, maxEndingHere, and minEndingHere all to numbers[0], and start your loop at index 1.
- ✕Mistake 3: Treating zeros as just another number and not understanding their reset effect — Symptom: the algorithm still works (zeros reset naturally via multiplication), but the candidate can't explain WHY to the interviewer, which costs them the offer — Fix: explicitly understand that multiplying any running product by 0 collapses both trackers to 0, and the 'start fresh with current element' candidate in Math.max/min handles the restart organically on the next iteration.
Interview Questions on This Topic
- QGiven the array [-2, 0, -1], what does your algorithm return and why? Walk me through it step by step. (Tests whether you understand the zero-reset and all-negative edge cases — the answer is 0, because zero itself is the best subarray product when all non-zero elements are negative single elements producing -1 and -2, both worse than 0.)
- QWhy do you need to track the minimum product as well as the maximum? Can you construct a specific example where ignoring the minimum gives the wrong answer?
- QHow would your solution change if the problem asked for the maximum product subarray of length at least 2? (Tests whether you can adapt DP state transitions under constraints — hint: you'd need to track whether you've used at least 2 elements, which adds a boolean state dimension to the DP.)
Frequently Asked Questions
What is the maximum product subarray problem?
It asks you to find the contiguous subarray within a given integer array that has the largest product when all its elements are multiplied together. The challenge is that negative numbers can flip the sign of the product, meaning the worst running product can suddenly become the best after one more multiplication.
Why do we need to track the minimum product in the maximum product subarray solution?
Because a very negative running product, when multiplied by another negative number, becomes a very large positive number — potentially the new global maximum. If you only track the maximum, you'll lose this 'rescue' scenario and return wrong answers on arrays with two or more negatives.
How does the maximum product subarray algorithm handle zeros?
When the algorithm encounters a zero, both maxEndingHere and minEndingHere become zero (anything times zero is zero). On the very next iteration, the three candidates are [nextElement, 0×nextElement=0, 0×nextElement=0], so the algorithm effectively restarts fresh from nextElement. No special-case code is needed — the mathematics of multiplication handles the reset automatically.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.