LeetCode 3362 - Zero Array Transformation III
We are given an integer array nums and a list of interval queries. Each query [l, r] allows us to reduce every element inside that range by at most 1. The important detail is that the decrement is optional and independent for every index in the range.
Difficulty: 🟡 Medium
Topics: Array, Two Pointers, Greedy, Sorting, Heap (Priority Queue), Prefix Sum
Solution
Problem Understanding
We are given an integer array nums and a list of interval queries. Each query [l, r] allows us to reduce every element inside that range by at most 1. The important detail is that the decrement is optional and independent for every index in the range.
This means a single query can contribute at most one unit of reduction to each covered index. If an index i is covered by k chosen queries, then we can reduce nums[i] by at most k.
The goal is not to minimize the number of queries we use directly. Instead, we want to remove as many queries as possible while still being able to transform the entire array into all zeroes using the remaining queries.
Another way to think about the problem is this:
- Every index
ineeds exactlynums[i]units of coverage. - Each query contributes
1unit of possible coverage to every position in its interval. - We want to keep the minimum number of queries that still satisfy all coverage requirements.
- The answer is:
total_queries - minimum_queries_needed
If even using all queries cannot provide enough coverage for some index, the answer is -1.
The constraints are large:
n <= 100000queries.length <= 100000
This immediately rules out expensive interval simulation approaches such as repeatedly updating ranges or trying subsets of queries.
Several edge cases are important:
- Some
nums[i]may already be0, meaning that index does not require any coverage. - Some indices may not be covered by enough queries at all, making the answer
-1. - Queries can overlap heavily.
- Multiple queries may start or end at the same position.
- A greedy choice that looks locally optimal can become globally inefficient if we choose short intervals too early.
The challenge is therefore to select the smallest set of intervals that collectively provide enough coverage for every position.
Approaches
Brute Force Approach
A brute force solution would try all subsets of queries and check whether the remaining queries can still reduce the array to zero.
For each subset:
- Count how many chosen queries cover every index.
- Verify whether coverage at index
iis at leastnums[i]. - Track the subset using the fewest queries.
This works because the condition for feasibility is simply coverage count per index.
However, the number of subsets is 2^m, where m is the number of queries. With up to 100000 queries, this is completely infeasible.
Even a less extreme brute force approach that greedily tests removals one by one would still require repeatedly recomputing interval coverage, leading to quadratic behavior.
Key Insight
The problem can be reframed as a coverage optimization problem.
At every index i, we need at least nums[i] active intervals covering that position.
Suppose we process indices from left to right. When we arrive at position i, we know:
- which chosen intervals are still active,
- how much coverage we already have,
- and whether we need additional intervals.
If more intervals are needed, the optimal greedy strategy is:
Choose the interval that extends farthest to the right.
Why?
Because longer intervals continue helping future positions. A short interval may satisfy the current index but expire quickly, forcing us to select additional intervals later.
This is a classic greedy interval covering principle.
To implement this efficiently:
- Sort queries by starting position.
- Use a max heap to store candidate intervals by ending position.
- Use another heap to track active chosen intervals.
- Use a difference-array style sweep to efficiently maintain current coverage.
This gives an O(m log m) solution.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^m * n) | O(n) | Tries every subset of queries |
| Optimal Greedy + Heap | O((n + m) log m) | O(m) | Greedy interval selection with priority queues |
Algorithm Walkthrough
- Sort all queries by their starting index.
This allows us to process intervals incrementally as we sweep from left to right across the array. 2. Maintain a max heap of candidate intervals.
For every position i, add all queries whose left endpoint is <= i.
We want to choose intervals with the largest right endpoint first, so we store negative right endpoints in Python's min heap to simulate a max heap. 3. Maintain the current active coverage count.
When we select a query, it contributes coverage from its start through its end.
Instead of updating every index in that range directly, we use a difference-array technique:
- Increment current coverage immediately.
- Schedule a future decrement at
r + 1.
- Remove expired active intervals.
Before processing position i, subtract any scheduled coverage removals.
5. Check whether current coverage satisfies nums[i].
If current coverage is already large enough, move on.
Otherwise, repeatedly select additional intervals from the candidate heap. 6. When selecting a new interval:
-
Choose the interval with the farthest right endpoint.
-
If its right endpoint is before
i, it cannot help anymore, so discard it. -
Otherwise:
-
increase active coverage,
-
schedule its expiration,
-
increment the count of used queries.
- If at some point we still cannot satisfy
nums[i], return-1.
This means even all available intervals are insufficient. 8. After processing all positions:
- Let
usedbe the minimum number of required queries. - Return:
total_queries - used
Why it works
The greedy choice is optimal because whenever additional coverage is required at position i, choosing the interval with the farthest right endpoint maximizes future usefulness.
Any shorter interval could be replaced with the farther-reaching interval without hurting feasibility, while potentially helping future indices more. Therefore, the greedy strategy never uses more intervals than necessary.
The sweep-line structure guarantees that coverage counts are always accurate for the current position.
Python Solution
from typing import List
import heapq
class Solution:
def maxRemoval(self, nums: List[int], queries: List[List[int]]) -> int:
queries.sort()
n = len(nums)
m = len(queries)
# Max heap using negative right endpoints
available = []
# Difference array for coverage expiration
diff = [0] * (n + 1)
current_coverage = 0
used_queries = 0
query_index = 0
for i in range(n):
current_coverage += diff[i]
# Add all queries starting at or before i
while query_index < m and queries[query_index][0] <= i:
l, r = queries[query_index]
heapq.heappush(available, -r)
query_index += 1
# Add more intervals if coverage is insufficient
while current_coverage < nums[i]:
# Remove unusable intervals
while available and -available[0] < i:
heapq.heappop(available)
if not available:
return -1
farthest_right = -heapq.heappop(available)
used_queries += 1
current_coverage += 1
if farthest_right + 1 < len(diff):
diff[farthest_right + 1] -= 1
return m - used_queries
The implementation begins by sorting the queries by starting position. This lets us progressively add intervals into the heap as we sweep across the array.
The available heap stores candidate intervals that can currently be chosen. Since Python's heapq is a min heap, we push negative right endpoints so that intervals with the farthest reach are chosen first.
The diff array implements lazy range updates. Instead of explicitly marking every covered position, we:
- immediately increase coverage when selecting an interval,
- then schedule a decrement after the interval ends.
At each index:
- expired effects are applied,
- newly available intervals are inserted,
- and additional intervals are greedily selected until the required coverage is met.
If no valid interval remains when additional coverage is required, the transformation is impossible and we return -1.
Finally, we subtract the number of used intervals from the total number of queries to determine how many can be removed.
Go Solution
package main
import (
"container/heap"
"sort"
)
type MaxHeap []int
func (h MaxHeap) Len() int {
return len(h)
}
func (h MaxHeap) Less(i, j int) bool {
return h[i] > h[j]
}
func (h MaxHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *MaxHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *MaxHeap) Pop() interface{} {
old := *h
n := len(old)
value := old[n-1]
*h = old[:n-1]
return value
}
func maxRemoval(nums []int, queries [][]int) int {
sort.Slice(queries, func(i, j int) bool {
if queries[i][0] == queries[j][0] {
return queries[i][1] < queries[j][1]
}
return queries[i][0] < queries[j][0]
})
n := len(nums)
m := len(queries)
diff := make([]int, n+1)
currentCoverage := 0
usedQueries := 0
queryIndex := 0
available := &MaxHeap{}
heap.Init(available)
for i := 0; i < n; i++ {
currentCoverage += diff[i]
for queryIndex < m && queries[queryIndex][0] <= i {
r := queries[queryIndex][1]
heap.Push(available, r)
queryIndex++
}
for currentCoverage < nums[i] {
for available.Len() > 0 && (*available)[0] < i {
heap.Pop(available)
}
if available.Len() == 0 {
return -1
}
farthestRight := heap.Pop(available).(int)
usedQueries++
currentCoverage++
if farthestRight+1 < len(diff) {
diff[farthestRight+1]--
}
}
}
return m - usedQueries
}
The Go solution follows the exact same greedy strategy as the Python version.
Since Go does not provide a built-in max heap, we implement one using the container/heap package and reverse the comparison in Less().
The difference array behaves identically to the Python implementation. Go slices are naturally dynamic enough for this problem size, and integer overflow is not a concern because all counts stay within 100000.
Worked Examples
Example 1
nums = [2,0,2]
queries = [[0,2],[0,2],[1,1]]
After sorting:
[[0,2],[0,2],[1,1]]
| i | nums[i] | Available Heap | Current Coverage | Action |
|---|---|---|---|---|
| 0 | 2 | [2,2] | 0 | Need 2 intervals |
| 0 | 2 | [2] | 1 | Pick interval [0,2] |
| 0 | 2 | [] | 2 | Pick interval [0,2] |
| 1 | 0 | [1] | 2 | Already enough |
| 2 | 2 | [1] | 2 | Already enough |
Used queries = 2
Total queries = 3
Answer:
3 - 2 = 1
Example 2
nums = [1,1,1,1]
queries = [[1,3],[0,2],[1,3],[1,2]]
Sorted:
[[0,2],[1,2],[1,3],[1,3]]
| i | nums[i] | Heap | Coverage | Action |
|---|---|---|---|---|
| 0 | 1 | [2] | 0 | Pick [0,2] |
| 1 | 1 | [3,3,2] | 1 | Enough |
| 2 | 1 | [3,3,2] | 1 | Enough |
| 3 | 1 | [3,3] | 0 | Pick [1,3] |
Used queries = 2
Total queries = 4
Answer:
4 - 2 = 2
Example 3
nums = [1,2,3,4]
queries = [[0,3]]
| i | nums[i] | Coverage Possible |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 2 | 1 |
At index 1, we need coverage 2 but only one interval exists.
Therefore the answer is:
-1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O((n + m) log m) | Each query is pushed and popped from the heap at most once |
| Space | O(m) | Heap and auxiliary structures store up to all queries |
The dominant cost comes from heap operations. Since every query enters and leaves the heap at most one time, the total number of heap operations is linear in the number of queries, each costing O(log m).
The difference-array sweep itself is linear.
Test Cases
from typing import List
class Solution:
def maxRemoval(self, nums: List[int], queries: List[List[int]]) -> int:
import heapq
queries.sort()
n = len(nums)
m = len(queries)
available = []
diff = [0] * (n + 1)
current_coverage = 0
used_queries = 0
query_index = 0
for i in range(n):
current_coverage += diff[i]
while query_index < m and queries[query_index][0] <= i:
heapq.heappush(available, -queries[query_index][1])
query_index += 1
while current_coverage < nums[i]:
while available and -available[0] < i:
heapq.heappop(available)
if not available:
return -1
r = -heapq.heappop(available)
used_queries += 1
current_coverage += 1
if r + 1 < len(diff):
diff[r + 1] -= 1
return m - used_queries
sol = Solution()
assert sol.maxRemoval([2,0,2], [[0,2],[0,2],[1,1]]) == 1 # example 1
assert sol.maxRemoval([1,1,1,1], [[1,3],[0,2],[1,3],[1,2]]) == 2 # example 2
assert sol.maxRemoval([1,2,3,4], [[0,3]]) == -1 # example 3
assert sol.maxRemoval([0,0,0], [[0,2]]) == 1 # all nums already zero
assert sol.maxRemoval([1], [[0,0]]) == 0 # single exact interval
assert sol.maxRemoval([2], [[0,0]]) == -1 # insufficient coverage
assert sol.maxRemoval([1,1], [[0,0],[1,1]]) == 0 # both intervals required
assert sol.maxRemoval([1,1], [[0,1],[0,1],[0,1]]) == 2 # only one interval needed
assert sol.maxRemoval([3,3,3], [[0,2],[0,2],[0,2]]) == 0 # all queries required
assert sol.maxRemoval([1,0,1], [[0,0],[2,2],[0,2]]) == 1 # greedy should prefer longer interval
assert sol.maxRemoval([2,1,2], [[0,2],[0,1],[1,2],[0,2]]) == 1 # overlapping intervals
Test Summary
| Test | Why |
|---|---|
[2,0,2] |
Verifies example 1 |
[1,1,1,1] |
Verifies example 2 |
[1,2,3,4] |
Verifies impossible case |
| All zeros | Ensures unnecessary queries are removable |
| Single element exact coverage | Smallest valid input |
| Single element insufficient coverage | Detects impossible coverage |
| Two disjoint intervals | Ensures both are required |
| Multiple redundant intervals | Tests maximum removals |
| Long interval preferred | Validates greedy correctness |
| Heavy overlap | Tests heap and coverage handling |
Edge Cases
One important edge case occurs when all elements in nums are already zero. In this situation, no queries are needed at all, so every query can be removed. A buggy implementation might still select intervals unnecessarily. The greedy coverage check prevents this because it only selects intervals when current coverage is below the required value.
Another critical edge case is insufficient total coverage. For example:
nums = [2]
queries = [[0,0]]
Even using every query, index 0 can only be decremented once. The implementation correctly detects this when the heap becomes empty before satisfying the required coverage.
A third subtle edge case involves overlapping intervals with different lengths. Suppose both short and long intervals can satisfy the current position. Choosing the short interval greedily may force additional selections later. The algorithm avoids this by always choosing the interval with the farthest right endpoint, guaranteeing maximal future usefulness.
Finally, expired intervals must be removed carefully. Intervals whose right endpoint is smaller than the current index can no longer contribute coverage. If these stale intervals remain in the heap, the algorithm may incorrectly assume valid coverage still exists. The implementation explicitly removes expired intervals before selecting new ones.