Maximum Product Subarray
- Track both curr_max and curr_min because negative × negative = positive.
- At each step, consider three candidates: num alone, num × curr_max, num × curr_min.
- A zero resets both curr_max and curr_min to the current number.
- At each index, compute three candidates: num alone, num × prev_max, num × prev_min
- curr_max = max of three candidates
- curr_min = min of three candidates
- When num is negative, swap max and min before computing (or use candidates tuple)
- Zero resets both to the element itself
- Update global_max = max(global_max, curr_max) each step
- Pricing engines with refund/penalty negatives must track both min and max product ranges
- A zero in sensor data resets the running product — the algorithm handles this naturally
- Tracking only max (Kadane's style). On [-2, 3, -4], Kadane's returns -4. Correct answer is 24.
Wrong answer on arrays with negatives ([-2, 3, -4] returns -4 instead of 24).
Print both: System.out.println("max=" + curr_max + " min=" + curr_min) at each stepTest [-2, 3, -4] — expected 24. If -4, min-tracking is missing.Zero in array resets everything, result is 0 even when larger product exists.
Test [2, 0, 3] — expected 3 (subarray [3] after zero).If result is 0, check that curr_max and curr_min are set to num (not 0) when num != 0 after a zero.Single-element array returns wrong answer ([-5] returns 0).
Test [-5] — expected -5.If result is 0, the initialization is global_max = 0. Change to nums[0].Production Incident
Production Debug GuideSymptom-first investigation path for product subarray bugs.
Maximum product subarray is a dynamic programming problem that looks like Kadane's algorithm for maximum sum subarray, but the twist is sign flipping. In sum-based problems, negatives only shrink the running value — you can safely ignore them. In product-based problems, two negatives multiply to a positive, so a previously terrible minimum can become the next step's maximum.
The correct approach tracks both max_prod and min_prod at every position. At each element, the new max is the maximum of (element alone, element × prev_max, element × prev_min). The new min is the minimum of the same three candidates. This single-pass O(n), O(1) algorithm handles positives, negatives, and zeros.
The common mistake is applying Kadane's algorithm directly — tracking only the running maximum. On input [-2, 3, -4], Kadane's returns -4. The correct answer is 24 (the entire array). This failure is deterministic and easy to reproduce, making it a high-signal interview question.
How Maximum Product Subarray Works — Step by Step
The key insight is that a negative number can become the maximum if multiplied by another negative. So we track both the maximum and minimum product ending at each position.
- Initialize max_prod = min_prod = result = nums[0].
- For each nums[i] starting at i=1:
- a. If nums[i] is negative, swap max_prod and min_prod (a previously small negative product becomes a large positive when multiplied by negative).
- b. max_prod = max(nums[i], max_prod * nums[i]).
- c. min_prod = min(nums[i], min_prod * nums[i]).
- d. result = max(result, max_prod).
- Return result.
For nums=[2,3,-2,4]: i=0: max=2, min=2, res=2. i=1: max=6, min=3, res=6. i=2: swap→ max=3,min=6; max=max(-2,3-2)=max(-2,-6)=-2; min=min(-2,6-2)=min(-2,-12)=-12; res=6. i=3: max=max(4,-24)=4; min=min(4,-124)=-48; res=6. Answer: 6 (subarray [2,3]).
package io.thecodeforge.algo; public class MaxProductSubarray { /** * Finds the contiguous subarray with the maximum product. * Tracks both max and min product ending at each position. * * Time: O(n) * Space: O(1) */ public static int maxProduct(int[] nums) { if (nums == null || nums.length == 0) { throw new IllegalArgumentException("Input array must not be empty"); } int currMax = nums[0]; int currMin = nums[0]; int globalMax = nums[0]; for (int i = 1; i < nums.length; i++) { int num = nums[i]; // Three candidates: start fresh, extend max, extend min // Compute all three before updating to avoid using updated values int candidateMax = Math.max(num, Math.max(currMax * num, currMin * num)); int candidateMin = Math.min(num, Math.min(currMax * num, currMin * num)); currMax = candidateMax; currMin = candidateMin; globalMax = Math.max(globalMax, currMax); } return globalMax; } public static void main(String[] args) { // Standard cases System.out.println(maxProduct(new int[]{2, 3, -2, 4})); // 6 — [2,3] System.out.println(maxProduct(new int[]{-2, 0, -1})); // 0 — zero resets System.out.println(maxProduct(new int[]{-2, 3, -4})); // 24 — [-2,3,-4] System.out.println(maxProduct(new int[]{2, -5, -2, -4, 3})); // 24 — [-5,-2,-4,3] // Edge cases System.out.println(maxProduct(new int[]{-5})); // -5 — single element System.out.println(maxProduct(new int[]{0})); // 0 — single zero System.out.println(maxProduct(new int[]{-2, -3, -4})); // 12 — [-2,-3,-4] System.out.println(maxProduct(new int[]{2, 0, 3})); // 3 — restart after zero } }
0
24
24
-5
0
12
3
- Sum DP: negatives only decrease. Track only max.
- Product DP: negatives flip sign. Track both max and min.
- The swap (or candidates tuple) handles the sign flip in one step.
- Zero is the neutralizer — it resets both max and min to zero.
- This pattern appears in any DP where the operation is not monotonic (addition is monotonic, multiplication is not).
Worked Example — Tracing with Negative Numbers
nums = [-2, 3, -4]. Expected answer: 24 (the entire array -23-4=24).
Step by step: 1. i=0: max_prod=-2, min_prod=-2, result=-2. 2. i=1 (value 3, positive, no swap): max_prod=max(3, -23)=max(3,-6)=3; min_prod=min(3,-23)=min(3,-6)=-6; result=3. 3. i=2 (value -4, negative → swap first): max_prod becomes -6, min_prod becomes 3. Then: max_prod=max(-4,-6-4)=max(-4,24)=24; min_prod=min(-4,3-4)=min(-4,-12)=-12; result=max(3,24)=24. 4. Answer: 24. The swap of max/min when the current element is negative is what enables the algorithm to correctly handle even counts of negatives.
package io.thecodeforge.algo; public class MaxProductSubarrayTraced { /** * Traced version — prints state at each step for debugging. */ public static int maxProductTraced(int[] nums) { if (nums == null || nums.length == 0) { throw new IllegalArgumentException("Input must not be empty"); } int currMax = nums[0]; int currMin = nums[0]; int globalMax = nums[0]; System.out.println("Step 0: num=" + nums[0] + " currMax=" + currMax + " currMin=" + currMin + " globalMax=" + globalMax); for (int i = 1; i < nums.length; i++) { int num = nums[i]; int candidateMax = Math.max(num, Math.max(currMax * num, currMin * num)); int candidateMin = Math.min(num, Math.min(currMax * num, currMin * num)); currMax = candidateMax; currMin = candidateMin; globalMax = Math.max(globalMax, currMax); System.out.println("Step " + i + ": num=" + num + " currMax=" + currMax + " currMin=" + currMin + " globalMax=" + globalMax); } return globalMax; } public static void main(String[] args) { System.out.println("=== Tracing [-2, 3, -4] ==="); int result = maxProductTraced(new int[]{-2, 3, -4}); System.out.println("Result: " + result); System.out.println("\n=== Tracing [2, -5, -2, -4, 3] ==="); result = maxProductTraced(new int[]{2, -5, -2, -4, 3}); System.out.println("Result: " + result); } }
Step 0: num=-2 currMax=-2 currMin=-2 globalMax=-2
Step 1: num=3 currMax=3 currMin=-6 globalMax=3
Step 2: num=-4 currMax=24 currMin=-12 globalMax=24
Result: 24
=== Tracing [2, -5, -2, -4, 3] ===
Step 0: num=2 currMax=2 currMin=2 globalMax=2
Step 1: num=-5 currMax=-5 currMin=-10 globalMax=2
Step 2: num=-2 currMax=20 currMin=-2 globalMax=20
Step 3: num=-4 currMax=8 currMin=-80 globalMax=20
Step 4: num=3 currMax=24 currMin=-240 globalMax=24
Result: 24
- Step 1: currMax=3, currMin=-6. The min (-6) is the compressed spring.
- Step 2: multiply by -4. The spring releases: -6 × -4 = 24. New currMax = 24.
- Without tracking min: currMax=3, max(3*-4, -4) = -4. The spring is never compressed.
- The trace makes this visible. Use it to debug.
- In production: log both max and min at each step when debugging product subarray issues.
The Solution — Track Both Min and Max
The critical insight: multiplying two negatives gives a positive, so today's minimum could become tomorrow's maximum. Maintain both max_product and min_product at each step. For each element: the new max is the maximum of (element alone, element prev_max, element prev_min). The new min is the minimum of the same three choices. When a zero is encountered, both reset to 0 or the element itself. Update the global answer with max_product at each step. This single-pass O(n), O(1) approach handles all cases: positives, negatives, and zeros.
package io.thecodeforge.algo; import java.util.Arrays; /** * Production-grade maximum product subarray implementation. * Tracks both max and min product ending at each position. * * Key invariant: at each step, currMax * currMin <= 0 is NOT guaranteed * (both can be negative if the element is negative). */ public class MaxProductSubarrayProduction { /** * Returns the maximum product of any contiguous subarray. * * @param nums array of integers (may contain negatives and zeros) * @return maximum product * @throws IllegalArgumentException if nums is null or empty */ public static int maxProduct(int[] nums) { if (nums == null || nums.length == 0) { throw new IllegalArgumentException("Input array must not be empty"); } // Initialize with first element — NOT zero int currMax = nums[0]; int currMin = nums[0]; int globalMax = nums[0]; for (int i = 1; i < nums.length; i++) { int num = nums[i]; // Compute all candidates BEFORE updating to avoid using stale values // The three candidates: start fresh, extend max streak, extend min streak int nextMax = Math.max(num, Math.max(currMax * num, currMin * num)); int nextMin = Math.min(num, Math.min(currMax * num, currMin * num)); currMax = nextMax; currMin = nextMin; globalMax = Math.max(globalMax, currMax); } return globalMax; } /** * Variant: returns the subarray itself, not just the product. * Tracks start/end indices alongside max/min products. */ public static int[] maxProductSubarray(int[] nums) { if (nums == null || nums.length == 0) { throw new IllegalArgumentException("Input array must not be empty"); } int currMax = nums[0]; int currMin = nums[0]; int globalMax = nums[0]; int maxStart = 0, maxEnd = 0; int currMaxStart = 0, currMinStart = 0; for (int i = 1; i < nums.length; i++) { int num = nums[i]; int nextMax = Math.max(num, Math.max(currMax * num, currMin * num)); int nextMin = Math.min(num, Math.min(currMax * num, currMin * num)); // Track where the max subarray starts if (nextMax == num) { currMaxStart = i; } else if (nextMax == currMin * num) { currMaxStart = currMinStart; } // else: nextMax == currMax * num, start unchanged if (nextMin == num) { currMinStart = i; } else if (nextMin == currMax * num) { currMinStart = currMaxStart; } currMax = nextMax; currMin = nextMin; if (currMax > globalMax) { globalMax = currMax; maxStart = currMaxStart; maxEnd = i; } } return Arrays.copyOfRange(nums, maxStart, maxEnd + 1); } public static void main(String[] args) { // Value-only System.out.println("Max product: " + maxProduct(new int[]{2, 3, -2, 4})); // 6 System.out.println("Max product: " + maxProduct(new int[]{-2, 3, -4})); // 24 System.out.println("Max product: " + maxProduct(new int[]{2, -5, -2, -4, 3})); // 24 // Subarray variant System.out.println("Max subarray: " + Arrays.toString( maxProductSubarray(new int[]{2, 3, -2, 4}))); System.out.println("Max subarray: " + Arrays.toString( maxProductSubarray(new int[]{-2, 3, -4}))); System.out.println("Max subarray: " + Arrays.toString( maxProductSubarray(new int[]{2, -5, -2, -4, 3}))); } }
Max product: 24
Max product: 24
Max subarray: [2, 3]
Max subarray: [-2, 3, -4]
Max subarray: [-5, -2, -4, 3]
- Swap approach: conditional branch, mutates state, can introduce bugs if swap is forgotten.
- Tuple approach: no branch, no mutation, all cases handled in one expression.
- Both produce the same result. Tuple is cleaner for production code.
- The swap approach is slightly faster (one branch vs two max/min calls), but the difference is negligible.
- In production: prefer clarity (tuple). In competitive programming: prefer speed (swap).
| Aspect | Maximum Sum (Kadane's) | Maximum Product (Dual Tracking) |
|---|---|---|
| State tracked | Only max_ending_here | Both max_ending_here AND min_ending_here |
| Why | Negatives only decrease sums — no sign flip | Negatives flip sign — min becomes max |
| Update rule | max(num, max_ending_here + num) | max(num, max_ending_here num, min_ending_here num) |
| Time complexity | O(n) | O(n) |
| Space complexity | O(1) | O(1) |
| Zero handling | Zero is a valid sum — no special case | Zero resets product — both max and min become 0 |
| Negative handling | Ignore or skip — negatives only shrink | Track min — min * negative = new max |
| Failure mode if wrong state | N/A — only max is needed | Kadane's returns -4 on [-2,3,-4] instead of 24 |
| Log transform | N/A | log(|x|) converts to sum, but breaks on zeros and negatives |
🎯 Key Takeaways
- Track both curr_max and curr_min because negative × negative = positive.
- At each step, consider three candidates: num alone, num × curr_max, num × curr_min.
- A zero resets both curr_max and curr_min to the current number.
- O(n) time, O(1) space — no DP array needed.
- The problem reduces to Kadane's on logarithms — but that breaks for zeros, so use the direct approach.
- The candidates tuple approach eliminates the need for explicit negative checks or swaps.
- Initialize with nums[0], not 0. Single-element arrays must return the element itself.
- The [-2, 3, -4] trace is the canonical test case. If your algorithm doesn't produce 24, it's broken.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhy must you track both maximum and minimum products in the maximum product subarray problem?
- QWhat is the time and space complexity of the optimal solution?
- QHow does the presence of zeros affect the maximum product subarray?
- QWalk me through the trace for [-2, 3, -4]. At step 2, how does the minimum become the maximum?
- QWhy can't you use the log transform to convert maximum product to maximum sum? What breaks?
- QHow would you modify the algorithm to return the subarray itself, not just the product?
- QKadane's algorithm works for maximum sum subarray. Why does it fail for maximum product? Give a concrete example.
- QHow would you handle the case where the input is a stream of numbers and you need to report the running maximum product?
- QWhat is the difference between maximum product subarray and maximum product subsequence? How does the algorithm change?
- QA pricing engine uses this algorithm but returns wrong results on inputs with negative multipliers. Walk me through your debugging process.
Frequently Asked Questions
Why does tracking only the maximum product fail?
Because multiplying by a negative flips the sign. If curr_max = -6 (very negative) and we multiply by -4, we get +24 — suddenly the minimum becomes the maximum. Without tracking the minimum, you miss this flip.
How does a zero affect the algorithm?
When you encounter a zero, num × curr_max = 0 and num × curr_min = 0. Both curr_max and curr_min reset to max(0, 0, num) and min(0, 0, num) = 0. This effectively restarts the search from scratch after the zero, which is correct — zero terminates any product subarray.
Why do we track both max and min products?
A negative number flips max to min and min to max. If the current minimum (a large negative) is multiplied by a new negative element, it becomes the new maximum. Without tracking both, we'd miss this case.
What happens when the array contains zeros?
Zero resets both max_product and min_product to zero. Any subarray containing a zero has product zero. The algorithm handles this naturally: max(0*anything, arr[i]) = arr[i] when arr[i] > 0, correctly starting a new subarray after the zero.
Why do we swap max_prod and min_prod when nums[i] is negative?
Multiplying by a negative flips the sign. The old minimum (most negative) × negative = largest positive. The old maximum (most positive) × negative = most negative. Swapping before the update ensures we compute the new max and min correctly in one step.
How does this differ from the maximum sum subarray (Kadane's) algorithm?
Kadane's tracks only one running value (max_ending_here) because sums can only grow by adding positive numbers — negatives only shrink. Products can flip sign, so negatives can be beneficial. That's why we track both max and min products at every position.
What happens when the array contains zeros?
A zero resets the product. When nums[i]=0, both max_prod and min_prod become max(0,0)=0 and min(0,0)=0. The next element starts fresh. If the maximum product subarray cannot avoid a zero, the answer might be 0 (handled because result is updated each step including when max_prod=0).
Why can't you use log transform to convert product to sum?
Log transform converts product to sum: log(a*b) = log(a) + log(b). But it breaks on zeros (log(0) = -infinity) and negatives (log of negative is undefined in reals). Since the problem includes both zeros and negatives, log transform is not viable. Use the direct dual-tracking approach.
How do you return the actual subarray, not just the product?
Track start and end indices alongside max and min products. When the new max comes from 'num alone', the subarray starts at the current index. When it comes from 'currMax num' or 'currMin num', the subarray extends from the corresponding start index. Update global max start/end when a new global maximum is found.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.