LeetCode 1567 - Maximum Length of Subarray With Positive Product
The problem asks us to find the length of the longest contiguous subarray whose product of elements is strictly positive. We are given an integer array nums, which may contain positive numbers, negative numbers, and zeros.
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Greedy
Solution
Problem Understanding
The problem asks us to find the length of the longest contiguous subarray whose product of elements is strictly positive.
We are given an integer array nums, which may contain positive numbers, negative numbers, and zeros. A subarray must consist of consecutive elements from the original array. Among all possible subarrays, we need to determine the maximum length where multiplying every element together produces a positive value.
The key observation is that the exact product value does not matter. We only care whether the product is positive or negative. This significantly simplifies the problem because the sign of a product depends entirely on the number of negative values:
- An even number of negative numbers produces a positive product.
- An odd number of negative numbers produces a negative product.
- Any subarray containing
0has product0, which is not positive.
The constraints are important:
nums.lengthcan be as large as10^5- Values themselves can be large, up to
10^9in magnitude
Because the array can contain one hundred thousand elements, any solution slower than linear or near-linear time will likely time out. Also, directly computing products is unnecessary and potentially dangerous because multiplication could overflow in some languages. Since only the sign matters, we should track positivity and negativity instead of actual product values.
Several edge cases are important:
- Arrays containing many zeros, because zeros split the array into independent segments
- Arrays containing only negative numbers
- Arrays where the best answer appears in the middle
- Arrays with alternating signs
- Single-element arrays, especially
[0],[1], and[-1]
A naive implementation that repeatedly computes products for every subarray would be far too slow.
Approaches
Brute Force Approach
The brute force solution checks every possible subarray. For each starting index, we extend the subarray one element at a time and compute whether the product is positive.
There are O(n^2) possible subarrays. If we explicitly multiply values for every subarray, the complexity becomes even worse. Even if we optimize by tracking only the sign, we still must inspect all subarrays.
The brute force method is correct because it exhaustively evaluates every candidate subarray and records the maximum valid length. However, with n = 10^5, an O(n^2) solution is far too slow.
Optimal Dynamic Programming / Greedy Observation
The important insight is that at every position, we only need to know:
- The length of the longest subarray ending at this index with positive product
- The length of the longest subarray ending at this index with negative product
We can process the array from left to right.
For each number:
- A positive number preserves signs
- A negative number flips signs
- Zero resets everything
This leads to a very compact dynamic programming approach using only two variables.
If:
positive_len= longest positive-product subarray ending herenegative_len= longest negative-product subarray ending here
Then we can update these values in constant time for each element.
This gives a linear-time solution.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Checks every possible subarray |
| Optimal | O(n) | O(1) | Tracks positive and negative subarray lengths dynamically |
Algorithm Walkthrough
- Initialize three variables:
positive_len = 0negative_len = 0max_length = 0
positive_len represents the longest subarray ending at the current index with positive product. negative_len represents the longest subarray ending at the current index with negative product.
2. Iterate through every number in the array from left to right.
3. If the current number is positive:
- Any existing positive-product subarray remains positive after appending this number.
- Any existing negative-product subarray remains negative after appending this number.
Therefore:
positive_len += 1negative_len = negative_len + 1ifnegative_len > 0
- If the current number is negative:
- Appending a negative number flips the sign.
- A previous positive-product subarray becomes negative.
- A previous negative-product subarray becomes positive.
Since both values depend on previous states, store them temporarily before updating.
Then:
positive_len = previous_negative_len + 1if previous negative existsnegative_len = previous_positive_len + 1
- If the current number is zero:
- Any subarray containing zero has product zero.
- No valid subarray can continue across zero.
Reset:
positive_len = 0negative_len = 0
- After processing each element, update:
max_length = max(max_length, positive_len)
- After the loop finishes, return
max_length.
Why it works
At every index, the algorithm maintains the exact lengths of the longest positive-product and negative-product subarrays ending at that position. These values fully capture all necessary information about previous elements because the sign of a product depends only on the parity of negative numbers. Since every update correctly transforms previous states based on the sign of the current number, the algorithm always maintains correct subproblem solutions and ultimately finds the global maximum.
Python Solution
from typing import List
class Solution:
def getMaxLen(self, nums: List[int]) -> int:
positive_len = 0
negative_len = 0
max_length = 0
for num in nums:
if num > 0:
positive_len += 1
if negative_len > 0:
negative_len += 1
elif num < 0:
previous_positive = positive_len
previous_negative = negative_len
if previous_negative > 0:
positive_len = previous_negative + 1
else:
positive_len = 0
negative_len = previous_positive + 1
else:
positive_len = 0
negative_len = 0
max_length = max(max_length, positive_len)
return max_length
The implementation directly follows the algorithm described earlier.
The variables positive_len and negative_len store the dynamic programming states for the current position. Instead of using arrays, we only keep the most recent values because each update depends only on the previous index.
When processing a positive number, signs do not change. Positive sequences grow longer, and negative sequences also extend if they already exist.
When processing a negative number, the signs flip. Because both values depend on previous states, temporary variables are used to avoid overwriting information before both updates are complete.
When encountering zero, both states reset because no subarray containing zero can have positive product.
The variable max_length continuously tracks the best positive-product subarray found so far.
Go Solution
func getMaxLen(nums []int) int {
positiveLen := 0
negativeLen := 0
maxLength := 0
for _, num := range nums {
if num > 0 {
positiveLen++
if negativeLen > 0 {
negativeLen++
}
} else if num < 0 {
previousPositive := positiveLen
previousNegative := negativeLen
if previousNegative > 0 {
positiveLen = previousNegative + 1
} else {
positiveLen = 0
}
negativeLen = previousPositive + 1
} else {
positiveLen = 0
negativeLen = 0
}
if positiveLen > maxLength {
maxLength = positiveLen
}
}
return maxLength
}
The Go implementation mirrors the Python logic closely.
Go uses explicit integer variables instead of Python's dynamically typed integers. Integer overflow is not a concern because we never compute actual products, only lengths.
The function operates directly on slices, which are efficient references to arrays. Since the algorithm uses constant extra space, no additional allocations are needed.
Worked Examples
Example 1
Input:
nums = [1, -2, -3, 4]
| Index | Value | positive_len | negative_len | max_length |
|---|---|---|---|---|
| 0 | 1 | 1 | 0 | 1 |
| 1 | -2 | 0 | 2 | 1 |
| 2 | -3 | 3 | 1 | 3 |
| 3 | 4 | 4 | 2 | 4 |
Final answer:
4
The entire array has positive product because there are two negative numbers.
Example 2
Input:
nums = [0, 1, -2, -3, -4]
| Index | Value | positive_len | negative_len | max_length |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 1 |
| 2 | -2 | 0 | 2 | 1 |
| 3 | -3 | 3 | 1 | 3 |
| 4 | -4 | 2 | 4 | 3 |
Final answer:
3
The best subarray is [1, -2, -3].
Example 3
Input:
nums = [-1, -2, -3, 0, 1]
| Index | Value | positive_len | negative_len | max_length |
|---|---|---|---|---|
| 0 | -1 | 0 | 1 | 0 |
| 1 | -2 | 2 | 1 | 2 |
| 2 | -3 | 2 | 3 | 2 |
| 3 | 0 | 0 | 0 | 2 |
| 4 | 1 | 1 | 0 | 2 |
Final answer:
2
The longest valid subarrays are [-1, -2] and [-2, -3].
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is processed exactly once |
| Space | O(1) | Only a few variables are maintained |
The algorithm scans the array one time from left to right. Every update takes constant time, so the total runtime grows linearly with the input size.
No auxiliary arrays, stacks, or hash maps are needed. The solution only stores a handful of integers, giving constant extra space usage.
Test Cases
from typing import List
class Solution:
def getMaxLen(self, nums: List[int]) -> int:
positive_len = 0
negative_len = 0
max_length = 0
for num in nums:
if num > 0:
positive_len += 1
if negative_len > 0:
negative_len += 1
elif num < 0:
previous_positive = positive_len
previous_negative = negative_len
if previous_negative > 0:
positive_len = previous_negative + 1
else:
positive_len = 0
negative_len = previous_positive + 1
else:
positive_len = 0
negative_len = 0
max_length = max(max_length, positive_len)
return max_length
solution = Solution()
assert solution.getMaxLen([1, -2, -3, 4]) == 4 # entire array positive
assert solution.getMaxLen([0, 1, -2, -3, -4]) == 3 # zero splits segments
assert solution.getMaxLen([-1, -2, -3, 0, 1]) == 2 # best before zero
assert solution.getMaxLen([1]) == 1 # single positive
assert solution.getMaxLen([-1]) == 0 # single negative
assert solution.getMaxLen([0]) == 0 # single zero
assert solution.getMaxLen([1, 2, 3, 4]) == 4 # all positive
assert solution.getMaxLen([-1, -2, -3, -4]) == 4 # even negatives
assert solution.getMaxLen([-1, -2, -3]) == 2 # odd negatives
assert solution.getMaxLen([0, 0, 0]) == 0 # all zeros
assert solution.getMaxLen([1, 0, 1, 0, 1]) == 1 # separated positives
assert solution.getMaxLen([-1, 2]) == 1 # single positive subarray
assert solution.getMaxLen([2, -1, 2, -1, 2]) == 5 # alternating signs
assert solution.getMaxLen([-1, 2, -3, 4, -5, 6]) == 5 # mixed signs
assert solution.getMaxLen([1, -2, 3, -4, 5, -6]) == 5 # multiple sign flips
| Test | Why |
|---|---|
[1, -2, -3, 4] |
Entire array has positive product |
[0, 1, -2, -3, -4] |
Tests reset behavior after zero |
[-1, -2, -3, 0, 1] |
Best subarray appears before zero |
[1] |
Single positive element |
[-1] |
Single negative element |
[0] |
Single zero element |
[1, 2, 3, 4] |
All positives |
[-1, -2, -3, -4] |
Even number of negatives |
[-1, -2, -3] |
Odd number of negatives |
[0, 0, 0] |
Multiple zero resets |
[1, 0, 1, 0, 1] |
Independent positive segments |
[-1, 2] |
Best answer is a single positive element |
[2, -1, 2, -1, 2] |
Alternating sign transitions |
[-1, 2, -3, 4, -5, 6] |
Multiple flips between positive and negative |
[1, -2, 3, -4, 5, -6] |
Long mixed-sign traversal |
Edge Cases
One important edge case is arrays containing zeros. Since any product involving zero becomes zero, valid subarrays cannot cross a zero. A common bug is forgetting to fully reset state variables after encountering zero. This implementation correctly resets both positive_len and negative_len to zero whenever a zero appears.
Another important case is arrays with an odd number of negative values. For example, [-1, -2, -3] has overall negative product, but smaller subarrays may still have positive product. A naive approach that only checks total parity for the entire segment would fail. The dynamic programming approach continuously tracks both positive and negative states, so it correctly identifies the best valid subarray.
A third edge case involves a negative number appearing when no previous negative-product subarray exists. In this situation, a new positive-product subarray cannot be formed immediately. The implementation carefully checks whether previous_negative > 0 before extending into a positive state. Without this condition, the algorithm could incorrectly create nonexistent subarrays.
A fourth subtle case is single-element arrays. Arrays like [1], [-1], and [0] test whether initialization and updates work correctly when the array length is minimal. The algorithm handles these naturally because the state transitions are valid even for one iteration.