3511. Make a Positive Array 🔒
Problem Description
You have an array nums
and need to determine the minimum number of operations to make it "positive". An array is considered positive when every subarray containing more than two elements has a positive sum.
The operation you can perform is:
- Replace any single element in
nums
with any integer value (between -10^18 and 10^18)
The goal is to find the minimum number of operations needed to ensure that all subarrays with length greater than 2 have positive sums.
For example, if you have a subarray of length 3 or more, its sum must be greater than 0 for the array to be considered positive. Subarrays of length 1 or 2 don't need to satisfy this condition.
The solution uses a greedy approach with a sliding window technique. It tracks:
l
: The left boundary (last position where an operation was performed)s
: Running sum of the current segmentpre_mx
: Maximum prefix sum that can be achieved by potentially replacing elementsans
: Count of operations needed
The algorithm iterates through the array and decides when to perform an operation. When a subarray of length > 2 would have a non-positive sum, it performs an operation (increments ans
) and resets the tracking variables to start a new segment.
The key insight is that we can greedily decide to "cut" the array at certain positions by performing operations, ensuring each segment between cuts maintains the positive sum property for all its subarrays of length > 2.
Intuition
The key observation is that when we replace an element with a very large positive number (like 10^18), we can effectively "break" the array into independent segments. Any subarray containing this large element will automatically have a positive sum, so we only need to worry about subarrays within each segment.
Think of it this way: if we place a huge positive number at position i
, then:
- Any subarray crossing position
i
will be positive (due to the huge value) - We only need to check subarrays entirely to the left of
i
or entirely to the right ofi
This means we can divide the problem into finding optimal "cut points" where we place these large values, creating segments that independently satisfy the condition.
For each segment between cuts, we need to ensure all its subarrays of length > 2 have positive sums. The critical insight is that if we're processing elements left to right and encounter a situation where adding the current element would create a subarray of length > 2 with non-positive sum, we should cut immediately.
Why is this greedy approach optimal? Because once we detect a violation (a subarray that would have non-positive sum), we must use at least one operation to fix it. The most efficient way is to cut right at the current position, which fixes all problematic subarrays ending at this position while giving us a fresh start for the next segment.
The variable pre_mx
tracks the maximum sum we could achieve by potentially modifying earlier elements in the current segment. When s <= pre_mx
for a subarray of length > 2, it means even with the best possible modification within the segment, we can't make all subarrays positive - so we must cut and start a new segment.
This greedy strategy of cutting as soon as we detect an unavoidable violation ensures we use the minimum number of operations.
Learn more about Greedy and Prefix Sum patterns.
Solution Approach
The solution implements a greedy algorithm with a sliding window technique to track segments and determine when to perform operations.
Variables Used:
l
: Marks the last position where we performed an operation (initially -1, meaning no operation yet)ans
: Counts the number of operations performedpre_mx
: Tracks the maximum prefix sum we could achieve in the current segments
: Running sum of elements in the current segmentr
: Current position in the array (right pointer)
Algorithm Steps:
-
Initialize variables: Start with
l = -1
(no previous cut),ans = 0
(no operations),pre_mx = 0
, ands = 0
. -
Iterate through the array: For each position
r
with valuex
:- Add current element to running sum:
s += x
- Add current element to running sum:
-
Check for violation: When we have a subarray of length > 2 (
r - l > 2
):- If
s <= pre_mx
, it means the current sum is not greater than what we could achieve by modifying previous elements - This indicates we cannot make all subarrays positive within this segment
- Perform an operation:
- Increment
ans += 1
- Set
l = r
(mark current position as cut point) - Reset
pre_mx = s = 0
for the new segment
- Increment
- If
-
Update maximum prefix: When we have at least 2 elements in the segment (
r - l >= 2
):- Update
pre_mx = max(pre_mx, s - x - nums[r-1])
- This represents the maximum sum we could get by potentially replacing the last two elements
- Update
Why This Works:
The algorithm essentially decides: "Can I continue the current segment, or must I cut here?"
- When
s <= pre_mx
for a subarray of length > 2, it means even if we optimally modified elements within the segment, we couldn't make all subarrays positive - By cutting at position
r
(replacingnums[r]
with a huge positive value), we ensure all subarrays crossing this position become positive - The greedy choice to cut as soon as a violation is detected ensures we use the minimum number of operations
Time Complexity: O(n)
- single pass through the array
Space Complexity: O(1)
- only using a constant amount of extra variables
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [1, -2, 3, -4, 2]
.
Initial State:
l = -1
,ans = 0
,pre_mx = 0
,s = 0
Step 1: r = 0, x = 1
- Add to sum:
s = 0 + 1 = 1
- Check violation:
r - l = 0 - (-1) = 1 ≤ 2
(no check needed) - Update pre_mx:
r - l = 1 < 2
(no update)
Step 2: r = 1, x = -2
- Add to sum:
s = 1 + (-2) = -1
- Check violation:
r - l = 1 - (-1) = 2 ≤ 2
(no check needed) - Update pre_mx:
r - l = 2 ≥ 2
, sopre_mx = max(0, -1 - (-2) - 1) = max(0, -1 + 2 - 1) = 0
Step 3: r = 2, x = 3
- Add to sum:
s = -1 + 3 = 2
- Check violation:
r - l = 2 - (-1) = 3 > 2
- Check if
s ≤ pre_mx
:2 ≤ 0
? No, so continue
- Check if
- Update pre_mx:
pre_mx = max(0, 2 - 3 - (-2)) = max(0, 1) = 1
Step 4: r = 3, x = -4
- Add to sum:
s = 2 + (-4) = -2
- Check violation:
r - l = 3 - (-1) = 4 > 2
- Check if
s ≤ pre_mx
:-2 ≤ 1
? Yes! Violation detected - Perform operation:
ans = 1
,l = 3
, resetpre_mx = 0
,s = 0
- Check if
- Update pre_mx:
r - l = 3 - 3 = 0 < 2
(no update)
Step 5: r = 4, x = 2
- Add to sum:
s = 0 + 2 = 2
- Check violation:
r - l = 4 - 3 = 1 ≤ 2
(no check needed) - Update pre_mx:
r - l = 1 < 2
(no update)
Result: ans = 1
The algorithm detected that continuing the segment would create the subarray [1, -2, 3, -4]
with sum -2
, which violates the positive sum requirement. By performing one operation at position 3 (replacing -4
with a large positive number), we split the array into two segments: [1, -2, 3]
and [2]
, both of which satisfy the condition.
Solution Implementation
1class Solution:
2 def makeArrayPositive(self, nums: List[int]) -> int:
3 # Initialize pointers and variables
4 left = -1 # Left boundary of current segment (exclusive)
5 operations_count = 0 # Count of operations needed
6 previous_max = 0 # Maximum sum of middle elements in previous windows
7 current_sum = 0 # Running sum of current segment
8
9 # Iterate through the array with index and value
10 for right, value in enumerate(nums):
11 # Add current element to running sum
12 current_sum += value
13
14 # Check if we have a segment of length > 2 and sum is not improving
15 if right - left > 2 and current_sum <= previous_max:
16 # Need an operation: reset the segment
17 operations_count += 1
18 left = right # Move left boundary to current position
19 previous_max = 0 # Reset previous maximum
20 current_sum = 0 # Reset current sum
21
22 # Update the maximum sum of middle elements for segments of length >= 2
23 elif right - left >= 2:
24 # Calculate sum excluding first and last elements of current segment
25 middle_sum = current_sum - value - nums[right - 1]
26 previous_max = max(previous_max, middle_sum)
27
28 return operations_count
29
1class Solution {
2 public int makeArrayPositive(int[] nums) {
3 int partitionCount = 0;
4 long previousMaxSum = 0; // Maximum sum of subarrays ending before current position
5 long currentSum = 0; // Running sum of current partition
6
7 // Use two pointers to track partition boundaries
8 int leftBoundary = -1; // Left boundary of current partition (exclusive)
9
10 for (int rightIndex = 0; rightIndex < nums.length; rightIndex++) {
11 int currentValue = nums[rightIndex];
12 currentSum += currentValue;
13
14 // Check if we need to create a new partition
15 // Condition 1: Current partition has more than 2 elements (rightIndex - leftBoundary > 2)
16 // Condition 2: Current sum is not greater than previous maximum sum
17 if (rightIndex - leftBoundary > 2 && currentSum <= previousMaxSum) {
18 // Start a new partition
19 partitionCount++;
20 leftBoundary = rightIndex;
21 previousMaxSum = 0;
22 currentSum = 0;
23 }
24 // Update the maximum sum for subarrays that can be formed
25 // Only update when partition has at least 2 elements
26 else if (rightIndex - leftBoundary >= 2) {
27 // Calculate max sum excluding the last two elements of current partition
28 // This represents the maximum sum of any valid subarray seen so far
29 long sumExcludingLastTwo = currentSum - currentValue - nums[rightIndex - 1];
30 previousMaxSum = Math.max(previousMaxSum, sumExcludingLastTwo);
31 }
32 }
33
34 return partitionCount;
35 }
36}
37
1class Solution {
2public:
3 int makeArrayPositive(vector<int>& nums) {
4 int segmentCount = 0; // Count of segments that need to be modified
5 long long maxPrefixSum = 0; // Maximum prefix sum seen in current segment
6 long long currentSum = 0; // Running sum of current segment
7
8 // Sliding window approach with left and right pointers
9 int leftIndex = -1; // Left boundary of current segment (exclusive)
10
11 for (int rightIndex = 0; rightIndex < nums.size(); rightIndex++) {
12 int currentValue = nums[rightIndex];
13 currentSum += currentValue;
14
15 // Check if current segment length > 2 and sum is not increasing
16 if (rightIndex - leftIndex > 2 && currentSum <= maxPrefixSum) {
17 // Need to start a new segment
18 segmentCount++;
19 leftIndex = rightIndex;
20 maxPrefixSum = 0;
21 currentSum = 0;
22 }
23 // Update max prefix sum for segments of length >= 2
24 else if (rightIndex - leftIndex >= 2) {
25 // Calculate prefix sum excluding last two elements
26 long long prefixWithoutLastTwo = currentSum - currentValue - nums[rightIndex - 1];
27 maxPrefixSum = max(maxPrefixSum, prefixWithoutLastTwo);
28 }
29 }
30
31 return segmentCount;
32 }
33};
34
1/**
2 * Counts the number of operations needed to make the array positive
3 * by removing subarrays when certain conditions are met
4 * @param nums - The input array of numbers
5 * @returns The number of operations performed
6 */
7function makeArrayPositive(nums: number[]): number {
8 // Left boundary pointer for the current window
9 let leftIndex: number = -1;
10
11 // operationCount: number of operations performed
12 // previousMaxSum: maximum sum of subarray ending 2 positions before current
13 // currentSum: running sum of current window
14 let operationCount: number = 0;
15 let previousMaxSum: number = 0;
16 let currentSum: number = 0;
17
18 // Iterate through the array with right pointer
19 for (let rightIndex: number = 0; rightIndex < nums.length; rightIndex++) {
20 const currentValue: number = nums[rightIndex];
21 currentSum += currentValue;
22
23 // Check if window size > 2 and current sum is not positive enough
24 if (rightIndex - leftIndex > 2 && currentSum <= previousMaxSum) {
25 // Perform an operation: reset the window
26 operationCount++;
27 leftIndex = rightIndex;
28 previousMaxSum = 0;
29 currentSum = 0;
30 }
31 // Update the maximum sum for subarrays ending 2 positions before
32 else if (rightIndex - leftIndex >= 2) {
33 // Calculate sum excluding last two elements
34 const sumWithoutLastTwo: number = currentSum - currentValue - nums[rightIndex - 1];
35 previousMaxSum = Math.max(previousMaxSum, sumWithoutLastTwo);
36 }
37 }
38
39 return operationCount;
40}
41
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a single pass through the input array nums
using a for loop that iterates through each element exactly once. Within each iteration, all operations are constant time:
- Array access operations (
nums[r-1]
) takeO(1)
- Arithmetic operations (addition, subtraction) take
O(1)
- Comparison operations and max function take
O(1)
- Variable assignments take
O(1)
Since we iterate through n
elements where n
is the length of the input array, and each iteration performs only constant time operations, the overall time complexity is O(n)
.
Space Complexity: O(1)
The algorithm uses only a fixed number of variables regardless of the input size:
- Integer variables:
l
,ans
,pre_mx
,s
,r
,x
All these variables occupy constant space. The algorithm doesn't create any additional data structures that scale with the input size. The input array is not modified and no auxiliary arrays or other collections are created. Therefore, the space complexity is O(1)
or constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary Handling After Operation
Pitfall: When performing an operation (cutting the segment), developers often incorrectly handle the boundary conditions. A common mistake is setting left = right - 1
instead of left = right
, or forgetting to reset the tracking variables properly.
# INCORRECT implementation if right - left > 2 and current_sum <= previous_max: operations_count += 1 left = right - 1 # Wrong! This creates an off-by-one error # Forgot to reset previous_max and current_sum
Why it's wrong: Setting left = right - 1
would mean the new segment starts including the element at position right
, but we've conceptually "replaced" this element to create the cut. The tracking variables also need to be reset to start fresh for the new segment.
Correct approach:
if right - left > 2 and current_sum <= previous_max: operations_count += 1 left = right # Correct: new segment starts after position right previous_max = 0 # Reset maximum prefix current_sum = 0 # Reset running sum
2. Misunderstanding the previous_max
Calculation
Pitfall: Incorrectly calculating what previous_max
represents, leading to wrong comparisons.
# INCORRECT calculation
elif right - left >= 2:
previous_max = max(previous_max, current_sum) # Wrong!
Why it's wrong: previous_max
should represent the maximum sum we could achieve by potentially replacing the last two elements in the current window. It's not just the maximum sum seen so far.
Correct approach:
elif right - left >= 2:
# Sum of middle elements (excluding first and last)
middle_sum = current_sum - value - nums[right - 1]
previous_max = max(previous_max, middle_sum)
3. Off-by-One Errors in Length Checks
Pitfall: Confusing when to check > 2
vs >= 2
for segment lengths.
# INCORRECT conditions if right - left >= 2 and current_sum <= previous_max: # Wrong comparison! # ... elif right - left > 2: # Wrong condition for update! # ...
Why it's wrong:
- We need to check violation when segment length is greater than 2 (subarrays of length > 2 must be positive)
- We update
previous_max
when segment length is at least 2 (we need at least 2 elements to have "middle" elements)
Correct approach:
if right - left > 2 and current_sum <= previous_max: # Check violation for segments longer than 2 elif right - left >= 2: # Update previous_max when we have at least 2 elements
4. Not Handling Edge Cases Properly
Pitfall: The algorithm might fail on edge cases like very small arrays or arrays with all negative/positive values.
Solution: Ensure the algorithm handles:
- Arrays with length ≤ 2 (should return 0 operations)
- Arrays where no operations are needed
- Arrays where multiple consecutive operations might be needed
The current implementation handles these correctly by:
- Starting with
left = -1
to properly handle the first elements - Only checking violations when segment length > 2
- Resetting properly after each operation to handle consecutive cuts
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!