Facebook Pixel

3362. Zero Array Transformation III

Problem Description

You have an integer array nums of length n and a 2D array queries where each queries[i] = [li, ri] represents a range operation.

Each query allows you to:

  • Decrement values in the range [li, ri] of nums by at most 1
  • You can choose independently for each index whether to decrement it by 0 or 1

Your goal is to convert nums into a Zero Array (all elements equal to 0) using some subset of the given queries.

The problem asks you to find the maximum number of queries you can remove from queries while still being able to convert nums to a zero array using only the remaining queries.

For example, if you have nums = [2, 0, 2] and queries covering different ranges, you need to determine which queries are essential for reducing all values to 0, and which ones can be removed. Each query can reduce values in its range by at most 1, so if nums[i] = 2, you need at least 2 queries that cover index i to reduce it to 0.

Return:

  • The maximum number of queries that can be removed if it's possible to achieve a zero array
  • -1 if it's impossible to convert nums to a zero array even using all queries

The key constraint is that each query can only reduce values by at most 1, so for any position i with value nums[i], you need at least nums[i] queries covering that position to reduce it to 0.

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

Intuition

Think about this problem inversely - instead of finding which queries to remove, let's find the minimum number of queries we must use to achieve a zero array. The maximum removals would then be total queries - minimum needed.

For each position i in nums, if nums[i] = k, we need at least k queries that cover position i to reduce it to 0. The challenge is selecting which queries to use efficiently.

Consider a greedy approach: as we traverse the array from left to right, at each position i, we want to ensure we have enough coverage. If we don't have enough coverage yet, which queries should we pick?

The key insight is that we should prefer queries that extend as far to the right as possible. Why? Because a query covering [l, r] with a larger r can help reduce values at more positions to the right, potentially saving us from using additional queries later.

This leads to our strategy:

  1. Process positions from left to right
  2. At each position, collect all queries that could start covering from this position (those with left endpoint ≤ i)
  3. If we need more coverage at position i, greedily select queries that end as late as possible (largest right endpoint)
  4. Keep track of when each selected query stops contributing (after its right endpoint)

We can use a max heap to efficiently track and select queries with the largest right endpoints. The difference array helps us maintain the running count of how many queries are currently active at each position.

The queries we never pop from the heap are the ones we "removed" - they weren't necessary for achieving the zero array. If at any position we can't get enough coverage even after using all available queries, it's impossible to achieve a zero array.

Learn more about Greedy, Prefix Sum, Sorting and Heap (Priority Queue) patterns.

Solution Approach

Let's implement the greedy strategy using a difference array and priority queue:

1. Sort queries by left endpoint:

queries.sort()

This ensures we can process queries in order as we traverse the array from left to right.

2. Initialize data structures:

  • pq = []: Max heap (using negative values in Python's min heap) to store right endpoints of candidate queries
  • d = [0] * (len(nums) + 1): Difference array to track coverage changes
  • s = 0: Current coverage count at position i
  • j = 0: Pointer to track which queries we've considered

3. Process each position from left to right:

for i, x in enumerate(nums):
    s += d[i]  # Update coverage count with difference array

The difference array tells us how the coverage changes at position i.

4. Add all queries starting at or before position i to the heap:

while j < len(queries) and queries[j][0] <= i:
    heappush(pq, -queries[j][1])  # Store negative for max heap
    j += 1

These are potential queries we could use if needed.

5. Greedily select queries when coverage is insufficient:

while s < x and pq and -pq[0] >= i:
    s += 1                      # Increase coverage
    d[-heappop(pq) + 1] -= 1   # Mark where this query ends
  • Check if current coverage s is less than required value x (which is nums[i])
  • Ensure the query at heap top still covers position i (-pq[0] >= i)
  • Pop the query with the largest right endpoint and apply it
  • Increment coverage immediately
  • Decrement coverage after the query's right endpoint using the difference array

6. Check feasibility:

if s < x:
    return -1

If we still don't have enough coverage after using all available queries, it's impossible.

7. Return the result:

return len(pq)

The queries remaining in the heap are those we didn't need to use - these are the "removed" queries.

Time Complexity: O(n log n + m log m) where n is the length of nums and m is the number of queries. We sort queries (O(m log m)) and potentially push/pop each query once from the heap (O(m log m)).

Space Complexity: O(n + m) for the difference array and heap.

The brilliance of this approach is that it makes locally optimal choices (picking the longest-reaching query when needed) that lead to a globally optimal solution, maximizing the number of unused queries.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through an example with nums = [2, 0, 2] and queries = [[0, 2], [0, 1], [1, 2]].

Initial Setup:

  • Sort queries by left endpoint: [[0, 2], [0, 1], [1, 2]] (already sorted)
  • Initialize: pq = [], d = [0, 0, 0, 0] (difference array), s = 0 (current coverage), j = 0 (query pointer)

Process position 0 (nums[0] = 2):

  1. Update coverage: s = s + d[0] = 0 + 0 = 0
  2. Add queries starting at or before position 0:
    • Query [0, 2]: push -2 to heap → pq = [-2]
    • Query [0, 1]: push -1 to heap → pq = [-2, -1]
  3. Need coverage of 2, but have 0. Select queries greedily:
    • First selection: Pop -2 (query [0, 2] with right endpoint 2)
      • s = 1, d[3] = -1 (coverage ends after position 2)
    • Second selection: Pop -1 (query [0, 1] with right endpoint 1)
      • s = 2, d[2] = -1 (coverage ends after position 1)
  4. Coverage satisfied: s = 2 ≥ nums[0] = 2

Process position 1 (nums[1] = 0):

  1. Update coverage: s = s + d[1] = 2 + 0 = 2
  2. Add queries starting at position 1:
    • Query [1, 2]: push -2 to heap → pq = [-2]
  3. Need coverage of 0, have 2. No queries needed ✓

Process position 2 (nums[2] = 2):

  1. Update coverage: s = s + d[2] = 2 + (-1) = 1 (one query ended after position 1)
  2. No new queries to add (all processed)
  3. Need coverage of 2, have 1. Select queries greedily:
    • Pop -2 (query [1, 2] with right endpoint 2)
      • s = 2, d[3] += -1d[3] = -2
  4. Coverage satisfied: s = 2 ≥ nums[2] = 2

Result:

  • Heap is empty: pq = []
  • All 3 queries were used, so 0 queries can be removed
  • Return: len(pq) = 0

This example shows how the algorithm greedily selects queries with the furthest right endpoints when needed, tracking coverage changes efficiently with the difference array.

Solution Implementation

1class Solution:
2    def maxRemoval(self, nums: List[int], queries: List[List[int]]) -> int:
3        # Sort queries by their start position for efficient processing
4        queries.sort()
5      
6        # Max heap to store end positions of available queries (negated for max heap)
7        available_queries_heap = []
8      
9        # Difference array to track range updates efficiently
10        # difference_array[i] represents the change at position i
11        difference_array = [0] * (len(nums) + 1)
12      
13        # Current sum tracking the cumulative effect of applied queries
14        current_coverage = 0
15      
16        # Pointer to track which queries have been processed
17        query_index = 0
18      
19        # Process each position in nums
20        for position, required_value in enumerate(nums):
21            # Update current coverage by applying the difference at this position
22            current_coverage += difference_array[position]
23          
24            # Add all queries that can start at or before current position to heap
25            while query_index < len(queries) and queries[query_index][0] <= position:
26                # Push negative end position for max heap behavior
27                heappush(available_queries_heap, -queries[query_index][1])
28                query_index += 1
29          
30            # Use queries greedily to satisfy the requirement at current position
31            while current_coverage < required_value and available_queries_heap and -available_queries_heap[0] >= position:
32                # Apply the query with the furthest valid end position
33                current_coverage += 1
34              
35                # Mark where this query's effect ends (at end_position + 1)
36                query_end_position = -heappop(available_queries_heap)
37                difference_array[query_end_position + 1] -= 1
38          
39            # Check if we can satisfy the requirement at current position
40            if current_coverage < required_value:
41                return -1
42      
43        # Return the number of unused queries remaining in the heap
44        return len(available_queries_heap)
45
1class Solution {
2    public int maxRemoval(int[] nums, int[][] queries) {
3        // Sort queries by their start position in ascending order
4        Arrays.sort(queries, (a, b) -> Integer.compare(a[0], b[0]));
5      
6        // Max heap to store end positions of available queries
7        // We prioritize queries that end later (can be used for more positions)
8        PriorityQueue<Integer> availableQueries = new PriorityQueue<>((a, b) -> b - a);
9      
10        int n = nums.length;
11        // Difference array to track when query effects end
12        // differenceArray[i] represents the change in active queries at position i
13        int[] differenceArray = new int[n + 1];
14      
15        // Current sum of active queries at position i
16        int activeQueryCount = 0;
17        // Index pointer for iterating through sorted queries
18        int queryIndex = 0;
19      
20        // Process each position in the array
21        for (int i = 0; i < n; i++) {
22            // Update the count of active queries at current position
23            activeQueryCount += differenceArray[i];
24          
25            // Add all queries that start at or before current position to available pool
26            while (queryIndex < queries.length && queries[queryIndex][0] <= i) {
27                availableQueries.offer(queries[queryIndex][1]);
28                queryIndex++;
29            }
30          
31            // Use queries from available pool if we need more to satisfy nums[i]
32            while (activeQueryCount < nums[i] && !availableQueries.isEmpty() && availableQueries.peek() >= i) {
33                // Use this query (increment active count)
34                activeQueryCount++;
35                // Mark where this query's effect ends (decrement at end + 1)
36                differenceArray[availableQueries.poll() + 1]--;
37            }
38          
39            // Check if we can satisfy the requirement at position i
40            if (activeQueryCount < nums[i]) {
41                return -1;  // Impossible to satisfy nums[i]
42            }
43        }
44      
45        // Return the maximum number of queries we didn't use
46        return availableQueries.size();
47    }
48}
49
1class Solution {
2public:
3    int maxRemoval(vector<int>& nums, vector<vector<int>>& queries) {
4        // Sort queries by their starting position for efficient processing
5        sort(queries.begin(), queries.end());
6      
7        // Max heap to store end positions of available queries
8        // We use max heap to prioritize queries that end later (more coverage)
9        priority_queue<int> availableQueries;
10      
11        int arraySize = nums.size();
12        // Difference array to track range updates efficiently
13        // differenceArray[i] represents the change in coverage at position i
14        vector<int> differenceArray(arraySize + 1, 0);
15      
16        // currentCoverage: tracks how many queries are currently covering position i
17        int currentCoverage = 0;
18        // queryIndex: pointer to iterate through sorted queries
19        int queryIndex = 0;
20      
21        // Process each position in the array
22        for (int position = 0; position < arraySize; ++position) {
23            // Update current coverage by applying the difference at this position
24            currentCoverage += differenceArray[position];
25          
26            // Add all queries that start at or before current position to available pool
27            while (queryIndex < queries.size() && queries[queryIndex][0] <= position) {
28                availableQueries.push(queries[queryIndex][1]);
29                ++queryIndex;
30            }
31          
32            // Use queries from the available pool if current coverage is insufficient
33            while (currentCoverage < nums[position] && !availableQueries.empty() && availableQueries.top() >= position) {
34                // Increment coverage for current position
35                ++currentCoverage;
36              
37                // Get the end position of the best available query
38                int queryEndPosition = availableQueries.top();
39                availableQueries.pop();
40              
41                // Mark where this query's coverage ends using difference array
42                // Decrement at endPosition + 1 to stop the coverage effect
43                --differenceArray[queryEndPosition + 1];
44            }
45          
46            // Check if we can satisfy the requirement at current position
47            if (currentCoverage < nums[position]) {
48                return -1;  // Impossible to satisfy all requirements
49            }
50        }
51      
52        // Return the number of unused queries (still in the priority queue)
53        return availableQueries.size();
54    }
55};
56
1/**
2 * Finds the maximum number of queries that can be removed while still being able to
3 * reduce all elements in nums to 0 or less
4 * @param nums - Array of integers to be reduced
5 * @param queries - Array of query ranges [left, right] that can reduce elements by 1
6 * @returns Maximum number of removable queries, or -1 if impossible
7 */
8function maxRemoval(nums: number[], queries: number[][]): number {
9    // Sort queries by their left boundary in ascending order
10    queries.sort((a, b) => a[0] - b[0]);
11  
12    // Priority queue to store right boundaries of available queries (max heap)
13    const priorityQueue = new MaxPriorityQueue<number>();
14  
15    const arrayLength = nums.length;
16  
17    // Difference array to track range updates efficiently
18    // differenceArray[i] represents the change at position i
19    const differenceArray: number[] = Array(arrayLength + 1).fill(0);
20  
21    // currentSum: running sum of the difference array (current reduction amount)
22    // queryIndex: pointer to track which queries have been processed
23    let currentSum = 0;
24    let queryIndex = 0;
25  
26    // Process each position in the nums array
27    for (let position = 0; position < arrayLength; position++) {
28        // Update the running sum with the difference at current position
29        currentSum += differenceArray[position];
30      
31        // Add all queries that start at or before current position to the priority queue
32        while (queryIndex < queries.length && queries[queryIndex][0] <= position) {
33            priorityQueue.enqueue(queries[queryIndex][1]);
34            queryIndex++;
35        }
36      
37        // Use queries greedily to satisfy the requirement at current position
38        // Pick queries with the largest right boundary that can still affect current position
39        while (currentSum < nums[position] && !priorityQueue.isEmpty() && priorityQueue.front() >= position) {
40            // Apply this query: increment current sum
41            currentSum++;
42            // Mark the end of this query's effect using difference array
43            differenceArray[priorityQueue.dequeue() + 1]--;
44        }
45      
46        // Check if we can satisfy the requirement at current position
47        if (currentSum < nums[position]) {
48            return -1;
49        }
50    }
51  
52    // Return the number of unused queries (maximum removable queries)
53    return priorityQueue.size();
54}
55

Time and Space Complexity

Time Complexity: O(n + m × log m)

The time complexity breaks down as follows:

  • Sorting the queries takes O(m × log m) where m is the number of queries
  • The main loop iterates through all n elements in nums, taking O(n) time
  • Within the main loop:
    • The inner while loop that adds queries to the heap runs at most m times total across all iterations, with each heappush operation taking O(log m)
    • The second inner while loop that pops from the heap also runs at most m times total across all iterations, with each heappop operation taking O(log m)
  • Total heap operations: at most m pushes and m pops, each taking O(log m), contributing O(m × log m)
  • Overall: O(m × log m) for sorting + O(n) for iteration + O(m × log m) for heap operations = O(n + m × log m)

Space Complexity: O(n + m)

The space complexity consists of:

  • The difference array d of size n + 1, taking O(n) space
  • The priority queue pq which can contain at most m elements (all queries), taking O(m) space
  • Other variables (s, j, i) use O(1) space
  • Total: O(n + m)

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

Common Pitfalls

1. Incorrectly Managing Query Availability

A critical pitfall is misunderstanding when queries become available versus when they can actually be used. Many implementations incorrectly assume that a query [l, r] can only be considered when processing index l, missing queries that started earlier but are still valid.

Wrong approach:

# Only considering queries that START at current position
if query[0] == position:
    heappush(heap, -query[1])

Correct approach:

# Consider ALL queries that START at or before current position
while query_index < len(queries) and queries[query_index][0] <= position:
    heappush(available_queries_heap, -queries[query_index][1])
    query_index += 1

2. Not Validating Query Coverage Before Using

Another common mistake is popping queries from the heap without checking if they still cover the current position. Since we're using a max heap based on end positions, a query might have the largest end position but might not actually cover the current index.

Wrong approach:

while current_coverage < required_value and available_queries_heap:
    # Blindly using the query without checking if it covers current position
    current_coverage += 1
    difference_array[-heappop(available_queries_heap) + 1] -= 1

Correct approach:

while current_coverage < required_value and available_queries_heap and -available_queries_heap[0] >= position:
    # Only use query if its end position covers current position
    current_coverage += 1
    difference_array[-heappop(available_queries_heap) + 1] -= 1

3. Difference Array Boundary Issues

A subtle but important pitfall is incorrectly updating the difference array, particularly at the boundaries. The effect of a query covering range [l, r] should end after position r, meaning we need to decrement at position r + 1.

Wrong approach:

# Incorrectly decrementing at position r
difference_array[query_end] -= 1

Correct approach:

# Correctly decrementing at position r + 1
difference_array[query_end_position + 1] -= 1

4. Premature Feasibility Check

Some implementations check if there are enough total queries to cover all positions before processing, but this doesn't account for the range constraints. Just because the sum of all query ranges exceeds the sum of nums doesn't guarantee feasibility.

Wrong approach:

# Check total coverage upfront
total_coverage = sum(r - l + 1 for l, r in queries)
if total_coverage < sum(nums):
    return -1

Correct approach:

# Check feasibility at each position during processing
if current_coverage < required_value:
    return -1

5. Forgetting to Sort Queries

The greedy algorithm relies on processing queries in order of their start positions. Forgetting to sort can lead to missed opportunities to use queries optimally.

Solution: Always sort queries by their left endpoint before processing:

queries.sort()  # Sorts by first element (left endpoint) by default
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More