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]
ofnums
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 convertnums
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.
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:
- Process positions from left to right
- At each position, collect all queries that could start covering from this position (those with
left endpoint ≤ i
) - If we need more coverage at position
i
, greedily select queries that end as late as possible (largest right endpoint) - 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 queriesd = [0] * (len(nums) + 1)
: Difference array to track coverage changess = 0
: Current coverage count at positioni
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 valuex
(which isnums[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 EvaluatorExample 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):
- Update coverage:
s = s + d[0] = 0 + 0 = 0
- 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]
- Query
- 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)
- First selection: Pop
- Coverage satisfied:
s = 2 ≥ nums[0] = 2
✓
Process position 1 (nums[1] = 0):
- Update coverage:
s = s + d[1] = 2 + 0 = 2
- Add queries starting at position 1:
- Query
[1, 2]
: push-2
to heap →pq = [-2]
- Query
- Need coverage of 0, have 2. No queries needed ✓
Process position 2 (nums[2] = 2):
- Update coverage:
s = s + d[2] = 2 + (-1) = 1
(one query ended after position 1) - No new queries to add (all processed)
- Need coverage of 2, have 1. Select queries greedily:
- Pop
-2
(query[1, 2]
with right endpoint 2)s = 2
,d[3] += -1
→d[3] = -2
- Pop
- 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)
wherem
is the number of queries - The main loop iterates through all
n
elements innums
, takingO(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 eachheappush
operation takingO(log m)
- The second inner while loop that pops from the heap also runs at most
m
times total across all iterations, with eachheappop
operation takingO(log m)
- The inner while loop that adds queries to the heap runs at most
- Total heap operations: at most
m
pushes andm
pops, each takingO(log m)
, contributingO(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 sizen + 1
, takingO(n)
space - The priority queue
pq
which can contain at mostm
elements (all queries), takingO(m)
space - Other variables (
s
,j
,i
) useO(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
Which two pointer techniques do you use to check if a string is a palindrome?
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!