Facebook Pixel

2599. Make the Prefix Sum Non-negative

Problem Description

You have a 0-indexed integer array nums. You can perform the following operation as many times as needed:

  • Pick any element from nums and move it to the end of the array.

A prefix sum array is defined as an array prefix where each element prefix[i] equals the sum of all elements from index 0 to i in the original array. In other words, prefix[i] = nums[0] + nums[1] + ... + nums[i].

Your goal is to find the minimum number of operations needed to rearrange the array such that no element in its prefix sum array is negative.

For example, if nums = [3, -5, 2]:

  • Initial prefix sum array: [3, -2, 0] (has negative value -2)
  • After moving -5 to the end: nums = [3, 2, -5]
  • New prefix sum array: [3, 5, 0] (all non-negative)
  • This takes 1 operation

The problem guarantees that it's always possible to achieve a non-negative prefix sum array through these operations.

The solution uses a greedy approach with a min heap. As we traverse the array and calculate running prefix sums, whenever the sum becomes negative, we greedily "undo" the most negative numbers we've seen so far (simulating moving them to the end) until the prefix sum becomes non-negative again. The heap keeps track of negative numbers encountered, allowing us to always remove the smallest (most negative) value first for optimal results.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we encounter a negative prefix sum while traversing the array, we need to fix it. The key insight is that moving an element to the end effectively "removes" it from all prefix sums up to that point. So when our running sum becomes negative, we want to remove some negative numbers we've already included to make the sum non-negative again.

But which negative numbers should we remove? Here's where the greedy choice comes in: we should always remove the most negative number available. Why? Because removing the smallest (most negative) number gives us the maximum increase in our prefix sum with just one operation. This minimizes the total number of operations needed.

Consider this: if our prefix sum is -3 and we have two negative numbers we could remove: -5 and -2. Removing -5 would change our sum from -3 to -3 - (-5) = 2, making it positive in one operation. Removing -2 would only get us to -1, still requiring another operation.

This naturally leads us to use a min heap to track all negative numbers we encounter. Whenever our prefix sum goes negative, we greedily pop the smallest value from the heap (the most negative number) and "undo" its contribution to the sum. We repeat this until the prefix sum becomes non-negative.

The beauty of this approach is that we're simulating the rearrangement without actually moving elements. Each pop from the heap represents moving that negative number to the end of the array, and we count each pop as one operation. The algorithm ensures we always make the optimal choice at each step, giving us the minimum number of operations needed.

Learn more about Greedy and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses a greedy algorithm with a min heap to efficiently track and remove the most negative numbers when needed.

Here's the step-by-step walkthrough:

  1. Initialize data structures:

    • h = []: A min heap to store negative numbers encountered
    • ans = 0: Counter for the number of operations
    • s = 0: Running prefix sum
  2. Process each element in the array:

    for x in nums:
        s += x  # Update the running prefix sum
  3. Track negative numbers:

    if x < 0:
        heappush(h, x)  # Add negative numbers to the min heap

    We only store negative numbers because these are the only ones we might need to move to fix negative prefix sums.

  4. Fix negative prefix sums greedily:

    while s < 0:
        s -= heappop(h)  # Remove the most negative number
        ans += 1         # Count this as one operation

    When the prefix sum becomes negative, we repeatedly:

    • Pop the smallest (most negative) value from the heap
    • Subtract it from s (since we're "removing" it from the prefix)
    • Increment our operation counter

    Note that subtracting a negative number increases s. For example, if s = -3 and we pop -5, then s = -3 - (-5) = 2.

  5. Return the total operations: After processing all elements, ans contains the minimum number of moves needed.

Time Complexity: O(n log n) where n is the length of the array. Each element is processed once, and heap operations take O(log n) time.

Space Complexity: O(n) in the worst case if all elements are negative and stored in the heap.

The algorithm ensures optimality because at each decision point, we make the locally optimal choice (removing the most negative number), which leads to the globally optimal solution for this problem.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with nums = [2, -3, -8, 4, -6, 1].

Initial Setup:

  • Min heap h = []
  • Operations count ans = 0
  • Running sum s = 0

Step 1: Process element 2

  • Update sum: s = 0 + 2 = 2
  • Since 2 is positive, don't add to heap
  • Sum is positive, no fix needed
  • State: s = 2, h = [], ans = 0

Step 2: Process element -3

  • Update sum: s = 2 + (-3) = -1
  • Since -3 is negative, add to heap: h = [-3]
  • Sum is negative! Fix it:
    • Pop smallest from heap: pop -3
    • Update sum: s = -1 - (-3) = 2
    • Increment operations: ans = 1
  • State: s = 2, h = [], ans = 1

Step 3: Process element -8

  • Update sum: s = 2 + (-8) = -6
  • Since -8 is negative, add to heap: h = [-8]
  • Sum is negative! Fix it:
    • Pop smallest from heap: pop -8
    • Update sum: s = -6 - (-8) = 2
    • Increment operations: ans = 2
  • State: s = 2, h = [], ans = 2

Step 4: Process element 4

  • Update sum: s = 2 + 4 = 6
  • Since 4 is positive, don't add to heap
  • Sum is positive, no fix needed
  • State: s = 6, h = [], ans = 2

Step 5: Process element -6

  • Update sum: s = 6 + (-6) = 0
  • Since -6 is negative, add to heap: h = [-6]
  • Sum is 0 (non-negative), no fix needed
  • State: s = 0, h = [-6], ans = 2

Step 6: Process element 1

  • Update sum: s = 0 + 1 = 1
  • Since 1 is positive, don't add to heap
  • Sum is positive, no fix needed
  • Final state: s = 1, h = [-6], ans = 2

Result: Minimum operations needed = 2

The algorithm effectively simulated moving -3 and -8 to the end of the array. The resulting arrangement would be [2, 4, -6, 1, -3, -8] with prefix sums [2, 6, 0, 1, -2, -10]. Note that we still have negative prefix sums at the end, but this is acceptable since we only care about maintaining non-negative prefix sums for the elements in their final positions before the moved elements.

Solution Implementation

1from typing import List
2import heapq
3
4class Solution:
5    def makePrefSumNonNegative(self, nums: List[int]) -> int:
6        # Min heap to store negative numbers (for greedy removal of most negative)
7        negative_heap = []
8
9        # operations_count: number of elements removed
10        # prefix_sum: running sum of elements not yet removed
11        operations_count = 0
12        prefix_sum = 0
13
14        # Process each number in the array
15        for current_num in nums:
16            # Add current number to the prefix sum
17            prefix_sum += current_num
18
19            # If current number is negative, add it to heap for potential removal
20            if current_num < 0:
21                heapq.heappush(negative_heap, current_num)
22
23            # While prefix sum is negative, remove the most negative numbers
24            while prefix_sum < 0:
25                # Remove the most negative number from consideration
26                most_negative = heapq.heappop(negative_heap)
27                prefix_sum -= most_negative  # Subtracting negative makes sum larger
28                operations_count += 1
29
30        return operations_count
31
1class Solution {
2    public int makePrefSumNonNegative(int[] nums) {
3        // Min heap to store negative numbers encountered so far
4        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
5
6        // Counter for number of operations performed
7        int operationCount = 0;
8
9        // Running prefix sum
10        long prefixSum = 0;
11
12        // Iterate through each element in the array
13        for (int currentNum : nums) {
14            // Update the prefix sum
15            prefixSum += currentNum;
16
17            // If current number is negative, add it to the min heap
18            if (currentNum < 0) {
19                minHeap.offer(currentNum);
20            }
21
22            // While prefix sum is negative, remove the most negative number
23            // from our previous selections to make the sum non-negative
24            while (prefixSum < 0) {
25                // Remove the smallest (most negative) number from consideration
26                prefixSum -= minHeap.poll();
27                // Increment operation count as we're effectively removing this number
28                operationCount++;
29            }
30        }
31
32        return operationCount;
33    }
34}
35
1class Solution {
2public:
3    int makePrefSumNonNegative(vector<int>& nums) {
4        // Min heap to store negative numbers encountered so far
5        // We use a min heap to greedily remove the most negative numbers first
6        priority_queue<int, vector<int>, greater<int>> minHeap;
7
8        // Count of operations (number of elements removed)
9        int operationCount = 0;
10
11        // Running prefix sum
12        long long prefixSum = 0;
13
14        // Iterate through each element in the array
15        for (int& num : nums) {
16            // Add current element to prefix sum
17            prefixSum += num;
18
19            // If current element is negative, add it to the min heap
20            // We track negative numbers as potential candidates for removal
21            if (num < 0) {
22                minHeap.push(num);
23            }
24
25            // While prefix sum is negative, remove the most negative elements
26            // This greedy approach ensures minimum number of removals
27            while (prefixSum < 0) {
28                // Remove the most negative element from consideration
29                prefixSum -= minHeap.top();
30                minHeap.pop();
31
32                // Increment the operation count
33                ++operationCount;
34            }
35        }
36
37        return operationCount;
38    }
39};
40
1/**
2 * Makes the prefix sum of the array non-negative by removing minimum elements
3 * @param nums - Input array of numbers
4 * @returns The minimum number of elements to remove
5 */
6function makePrefSumNonNegative(nums: number[]): number {
7    // Min-heap to store negative numbers encountered
8    const minHeap = new MinPriorityQueue();
9
10    // Counter for number of elements removed
11    let removedCount = 0;
12
13    // Running prefix sum
14    let prefixSum = 0;
15
16    // Iterate through each element in the array
17    for (const num of nums) {
18        // Update the prefix sum
19        prefixSum += num;
20
21        // If current number is negative, add it to the min-heap
22        if (num < 0) {
23            minHeap.enqueue(num);
24        }
25
26        // While prefix sum is negative, remove the most negative numbers
27        while (prefixSum < 0) {
28            // Remove the smallest (most negative) number from consideration
29            const smallestNegative = minHeap.dequeue().element;
30            prefixSum -= smallestNegative;
31            removedCount++;
32        }
33    }
34
35    return removedCount;
36}
37

Time and Space Complexity

Time Complexity: O(n × log n)

The algorithm iterates through the array once with n elements. For each element:

  • Adding an element to the array takes O(1) time
  • If the element is negative, it's pushed to the heap in O(log k) time where k is the current heap size
  • The while loop may execute multiple times, and each heappop operation takes O(log k) time

In the worst case, all negative numbers could be pushed to the heap and then popped, resulting in up to n heap operations. Since each heap operation (push or pop) takes O(log n) time in the worst case when the heap contains up to n elements, the overall time complexity is O(n × log n).

Space Complexity: O(n)

The space is dominated by the min-heap h, which in the worst case stores all negative numbers from the input array. If all n elements are negative, the heap would contain up to n elements, resulting in O(n) space complexity. The other variables (ans, s, x) use constant space O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Confusion About the "Removal" Operation

The Issue: A common misunderstanding is thinking that when we pop a negative number from the heap and "remove" it, we're deleting it from the array entirely. This leads to confusion about why we subtract the popped value from the prefix sum instead of just recalculating.

What Actually Happens: The operation doesn't delete elements; it moves them to the end of the array. When we pop a value from the heap, we're simulating that this element will be moved to the end later, so we need to exclude it from our current prefix sum calculation.

Correct Understanding:

# When we do this:
most_negative = heapq.heappop(negative_heap)
prefix_sum -= most_negative  # This is correct!

# We're effectively saying: "pretend this element isn't here yet"
# Since most_negative is negative, subtracting it increases prefix_sum
# Example: prefix_sum = -2, most_negative = -5
# prefix_sum = -2 - (-5) = 3

Pitfall 2: Attempting to Track Element Positions

The Issue: Some might try to track which specific elements have been "moved" and their original positions, thinking this information is necessary for the solution.

Why It's Wrong:

# INCORRECT approach:
moved_indices = set()
for i, x in enumerate(nums):
    if should_move:
        moved_indices.add(i)  # Unnecessary complexity!

The Solution: The beauty of this greedy approach is that we don't need to track positions. We only need to know:

  1. Which negative values we've encountered (stored in heap)
  2. How many we've decided to move (the count)

The actual rearrangement details are irrelevant for counting operations.

Pitfall 3: Forgetting to Handle Only Negative Numbers in Heap

The Issue: Adding all numbers to the heap, not just negative ones:

# INCORRECT:
for current_num in nums:
    heapq.heappush(negative_heap, current_num)  # Wrong! Adds positives too

Why It Matters:

  • We never need to move positive numbers to fix negative prefix sums
  • Moving positive numbers would only make prefix sums smaller (counterproductive)
  • Including them wastes space and could lead to incorrect logic if we accidentally pop a positive number

Correct Approach:

if current_num < 0:
    heapq.heappush(negative_heap, current_num)  # Only negatives

Pitfall 4: Not Understanding the Heap's Role

The Issue: Using a different data structure or not understanding why a min heap specifically is needed:

# INCORRECT alternatives:
negative_list = []  # Using a list and sorting each time - O(n²log n)
negative_stack = []  # LIFO doesn't give us the most negative element

Why Min Heap is Optimal:

  • We need the most negative number (minimum value) quickly
  • Min heap gives us O(log n) access to the smallest element
  • This ensures we make the maximum impact on the prefix sum with each operation
  • Removing -10 is better than removing -1 when trying to fix a negative prefix sum
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More