3356. Zero Array Transformation II
Problem Description
You are given an integer array nums
of length n
and a 2D array queries
where each query is represented as queries[i] = [li, ri, vali]
.
For each query, you can perform the following action on nums
:
- Choose any value between 0 and
vali
(inclusive) for each index in the range[li, ri]
- Subtract the chosen value from the element at that index
- The amount subtracted can be different for each index within the range
A Zero Array is defined as an array where all elements equal 0.
Your task is to find the minimum number k
such that after processing the first k
queries in sequence, you can transform nums
into a Zero Array. If it's impossible to achieve a Zero Array using any number of the given queries, return -1.
For example, if you have nums = [3, 2, 1]
and a query [0, 2, 2]
, you could:
- Subtract 2 from index 0 (3 becomes 1)
- Subtract 2 from index 1 (2 becomes 0)
- Subtract 1 from index 2 (1 becomes 0)
The key insight is that each query provides a "budget" of reductions you can apply flexibly across the specified range, and you need to determine how many queries (processed in order) are sufficient to reduce all elements to zero.
Intuition
The key observation is that this problem exhibits monotonic behavior: if we can transform the array into a Zero Array using the first k
queries, then we can definitely do it with the first k+1
queries (or any number greater than k
). This is because having more queries only gives us more "reduction power" - we never lose the ability to reduce elements.
This monotonicity immediately suggests binary search on the answer. Instead of checking every possible value of k
from 1 to m
(where m
is the total number of queries), we can use binary search to efficiently find the minimum k
.
For checking whether a specific number of queries k
is sufficient, we need to determine the maximum reduction available at each index after applying the first k
queries. Here's where the difference array technique becomes crucial:
- Each query
[l, r, val]
addsval
units of reduction capacity to all indices in range[l, r]
- Multiple queries can overlap, so the total reduction capacity at each index is cumulative
- We can efficiently compute these cumulative values using a difference array
The difference array works by marking the start and end of each range:
- Add
val
at positionl
(start of range) - Subtract
val
at positionr+1
(just after end of range) - When we compute the prefix sum, we get the actual reduction capacity at each position
For each index i
, if nums[i]
is greater than the total reduction capacity available at that index, then k
queries are insufficient. Otherwise, we can strategically use the available reduction to bring nums[i]
down to zero.
This approach transforms the problem from "how should we optimally apply each query" to "is the total reduction capacity at each index sufficient", which is much simpler to verify.
Learn more about Binary Search and Prefix Sum patterns.
Solution Approach
The solution implements binary search combined with a difference array to efficiently find the minimum number of queries needed.
Binary Search Setup:
We use binary search to find the minimum k
in the range [0, m]
where m
is the total number of queries. The bisect_left
function searches for the first value where check(k)
returns True
.
The Check Function:
The check(k)
function determines if the first k
queries provide enough reduction capacity to transform nums
into a Zero Array.
-
Initialize Difference Array: Create an array
d
of lengthn + 1
filled with zeros. This will track the reduction capacity changes. -
Process First k Queries: For each query
[l, r, val]
in the firstk
queries:- Add
val
tod[l]
(marking the start of the range) - Subtract
val
fromd[r + 1]
(marking the end of the range)
- Add
-
Compute Actual Reduction Capacity:
- Initialize a running sum
s = 0
- Iterate through each index
i
from 0 ton-1
- Add
d[i]
to the running sums
- At each position,
s
represents the total reduction capacity available - If
nums[i] > s
, it means we cannot reducenums[i]
to zero with the available capacity, so returnFalse
- Initialize a running sum
-
Return Result: If we successfully process all indices without finding any insufficiency, return
True
.
Final Answer:
The binary search finds the leftmost position l
where check(l)
is True
.
- If
l > m
, it means even using all queries isn't sufficient, so return-1
- Otherwise, return
l
as the minimum number of queries needed
Time Complexity: O(m * log(m) * n)
where m
is the number of queries and n
is the length of nums
. The binary search runs O(log m)
iterations, and each check operation takes O(m + n)
time.
Space Complexity: O(n)
for the difference array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Given:
nums = [3, 2, 1]
queries = [[0, 1, 2], [1, 2, 1], [0, 2, 1]]
We need to find the minimum number of queries k
such that we can transform nums
into a Zero Array.
Step 1: Binary Search Setup
We'll binary search on the range [0, 3]
(since we have 3 queries total).
Step 2: Check if k=2 queries are sufficient
Let's check if the first 2 queries [[0, 1, 2], [1, 2, 1]]
provide enough reduction capacity.
Building the Difference Array:
- Initialize:
d = [0, 0, 0, 0]
(length n+1 = 4) - Query 1
[0, 1, 2]
:d[0] += 2
→d = [2, 0, 0, 0]
d[2] -= 2
→d = [2, 0, -2, 0]
- Query 2
[1, 2, 1]
:d[1] += 1
→d = [2, 1, -2, 0]
d[3] -= 1
→d = [2, 1, -2, -1]
Computing Reduction Capacity via Prefix Sum:
- Index 0:
s = 0 + d[0] = 0 + 2 = 2
- Check:
nums[0] = 3 > 2
? Yes! Cannot reduce 3 to 0 with only 2 units. - Return
False
- Check:
Since k=2 is insufficient, binary search will try a larger value.
Step 3: Check if k=3 queries are sufficient
Using all 3 queries [[0, 1, 2], [1, 2, 1], [0, 2, 1]]
:
Building the Difference Array:
- Initialize:
d = [0, 0, 0, 0]
- Query 1
[0, 1, 2]
:d = [2, 0, -2, 0]
- Query 2
[1, 2, 1]
:d = [2, 1, -2, -1]
- Query 3
[0, 2, 1]
:d[0] += 1
→d = [3, 1, -2, -1]
d[3] -= 1
→d = [3, 1, -2, -2]
Computing Reduction Capacity:
- Index 0:
s = 0 + 3 = 3
- Check:
nums[0] = 3 ≤ 3
? Yes! Can reduce to 0.
- Check:
- Index 1:
s = 3 + 1 = 4
- Check:
nums[1] = 2 ≤ 4
? Yes! Can reduce to 0.
- Check:
- Index 2:
s = 4 + (-2) = 2
- Check:
nums[2] = 1 ≤ 2
? Yes! Can reduce to 0.
- Check:
- Return
True
Step 4: Binary Search Conclusion The binary search finds that:
- k=2 is insufficient
- k=3 is sufficient
Therefore, the minimum k is 3.
Key Insight: The difference array elegantly tracks how reduction capacity accumulates across overlapping ranges. When we compute the prefix sum, we get the exact total reduction available at each position, making it easy to verify if we have enough "budget" to reduce each element to zero.
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4class Solution:
5 def minZeroArray(self, nums: List[int], queries: List[List[int]]) -> int:
6 """
7 Find the minimum number of queries needed to make all elements in nums <= 0.
8 Each query [left, right, val] adds val to all elements from index left to right.
9 Returns -1 if impossible.
10 """
11
12 def can_make_zero(num_queries: int) -> bool:
13 """
14 Check if using the first num_queries queries can make all elements <= 0.
15 Uses difference array technique for efficient range updates.
16 """
17 # Initialize difference array with one extra element for boundary
18 diff_array = [0] * (len(nums) + 1)
19
20 # Apply first num_queries queries using difference array technique
21 for left, right, value in queries[:num_queries]:
22 diff_array[left] += value # Add value at start of range
23 diff_array[right + 1] -= value # Subtract value after end of range
24
25 # Check if each element in nums can be reduced to <= 0
26 cumulative_sum = 0
27 for original_value, diff_value in zip(nums, diff_array):
28 cumulative_sum += diff_value # Get actual value added at this position
29 if original_value > cumulative_sum: # Check if original value minus additions > 0
30 return False
31
32 return True
33
34 # Binary search for minimum number of queries needed
35 total_queries = len(queries)
36
37 # Find leftmost position where can_make_zero returns True
38 # Search range is [0, total_queries] inclusive
39 min_queries_needed = bisect_left(range(total_queries + 1), True, key=can_make_zero)
40
41 # If min_queries_needed > total_queries, it's impossible
42 return -1 if min_queries_needed > total_queries else min_queries_needed
43
1class Solution {
2 private int arrayLength;
3 private int[] originalArray;
4 private int[][] queryArray;
5
6 /**
7 * Finds the minimum number of queries needed to make all elements in nums array zero or less.
8 * Uses binary search to find the minimum k such that applying first k queries is sufficient.
9 *
10 * @param nums The original array of integers
11 * @param queries Array of queries where each query [l, r, val] decreases elements from index l to r by val
12 * @return Minimum number of queries needed, or -1 if impossible
13 */
14 public int minZeroArray(int[] nums, int[][] queries) {
15 this.originalArray = nums;
16 this.queryArray = queries;
17 this.arrayLength = nums.length;
18 int totalQueries = queries.length;
19
20 // Binary search on the number of queries to use
21 // Search range: [0, totalQueries + 1)
22 int left = 0;
23 int right = totalQueries + 1;
24
25 while (left < right) {
26 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
27
28 if (canMakeZeroArray(mid)) {
29 // If k queries are sufficient, try fewer queries
30 right = mid;
31 } else {
32 // If k queries are not sufficient, need more queries
33 left = mid + 1;
34 }
35 }
36
37 // If left > totalQueries, it means even using all queries is not sufficient
38 return left > totalQueries ? -1 : left;
39 }
40
41 /**
42 * Checks if applying the first k queries can make all elements in nums array zero or less.
43 * Uses difference array technique for efficient range updates.
44 *
45 * @param k Number of queries to apply
46 * @return true if first k queries are sufficient, false otherwise
47 */
48 private boolean canMakeZeroArray(int k) {
49 // Difference array for range updates
50 // differenceArray[i] represents the change at index i
51 int[] differenceArray = new int[arrayLength + 1];
52
53 // Apply first k queries using difference array technique
54 for (int i = 0; i < k; i++) {
55 int leftIndex = queryArray[i][0];
56 int rightIndex = queryArray[i][1];
57 int decrementValue = queryArray[i][2];
58
59 // Mark the start and end of range update
60 differenceArray[leftIndex] += decrementValue;
61 differenceArray[rightIndex + 1] -= decrementValue;
62 }
63
64 // Calculate actual values after applying all k queries
65 // Use prefix sum to get the actual decrement at each position
66 int cumulativeDecrement = 0;
67 for (int i = 0; i < arrayLength; i++) {
68 cumulativeDecrement += differenceArray[i];
69
70 // Check if the original value minus total decrement is still positive
71 if (originalArray[i] > cumulativeDecrement) {
72 return false; // Cannot make this element zero or less
73 }
74 }
75
76 return true; // All elements can be made zero or less
77 }
78}
79
1class Solution {
2public:
3 int minZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
4 int n = nums.size();
5 int totalQueries = queries.size();
6
7 // Binary search range: [0, totalQueries + 1]
8 // We search for the minimum number of queries needed
9 int left = 0, right = totalQueries + 1;
10
11 // Lambda function to check if k queries are sufficient to make all elements <= 0
12 auto canMakeZero = [&](int k) -> bool {
13 // Difference array for range updates
14 // differenceArray[i] represents the change at position i
15 int differenceArray[n + 1];
16 memset(differenceArray, 0, sizeof(differenceArray));
17
18 // Apply first k queries using difference array technique
19 for (int i = 0; i < k; ++i) {
20 int rangeLeft = queries[i][0];
21 int rangeRight = queries[i][1];
22 int decrementValue = queries[i][2];
23
24 // Mark the start and end of the range update
25 differenceArray[rangeLeft] += decrementValue;
26 differenceArray[rangeRight + 1] -= decrementValue;
27 }
28
29 // Calculate actual values after applying all k queries
30 // and check if each element can be reduced to 0 or below
31 int cumulativeSum = 0;
32 for (int i = 0; i < n; ++i) {
33 cumulativeSum += differenceArray[i];
34 // If current element minus total decrement is still positive,
35 // k queries are not sufficient
36 if (nums[i] > cumulativeSum) {
37 return false;
38 }
39 }
40 return true;
41 };
42
43 // Binary search for the minimum number of queries
44 while (left < right) {
45 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
46
47 if (canMakeZero(mid)) {
48 // If mid queries are sufficient, try to find a smaller number
49 right = mid;
50 } else {
51 // If mid queries are not sufficient, we need more queries
52 left = mid + 1;
53 }
54 }
55
56 // If left > totalQueries, it means even all queries are not sufficient
57 return left > totalQueries ? -1 : left;
58 }
59};
60
1/**
2 * Finds the minimum number of queries needed to make all elements in nums array zero or negative
3 * @param nums - Array of numbers to be reduced
4 * @param queries - Array of queries, each query [l, r, val] adds val to nums[l..r]
5 * @returns Minimum number of queries needed, or -1 if impossible
6 */
7function minZeroArray(nums: number[], queries: number[][]): number {
8 const numsLength: number = nums.length;
9 const queriesLength: number = queries.length;
10
11 // Difference array for range updates
12 const differenceArray: number[] = Array(numsLength + 1);
13
14 // Binary search bounds
15 let left: number = 0;
16 let right: number = queriesLength + 1;
17
18 /**
19 * Checks if first k queries are sufficient to make all elements zero or negative
20 * @param k - Number of queries to use
21 * @returns true if k queries are sufficient, false otherwise
22 */
23 const canMakeZeroWithKQueries = (k: number): boolean => {
24 // Reset difference array
25 differenceArray.fill(0);
26
27 // Apply first k queries using difference array technique
28 for (let i = 0; i < k; i++) {
29 const [leftIndex, rightIndex, value] = queries[i];
30 differenceArray[leftIndex] += value;
31 differenceArray[rightIndex + 1] -= value;
32 }
33
34 // Calculate actual values after applying queries and check if sufficient
35 let cumulativeSum: number = 0;
36 for (let i = 0; i < numsLength; i++) {
37 cumulativeSum += differenceArray[i];
38 // If any element remains positive after applying queries, return false
39 if (nums[i] > cumulativeSum) {
40 return false;
41 }
42 }
43
44 return true;
45 };
46
47 // Binary search for minimum number of queries
48 while (left < right) {
49 const mid: number = (left + right) >> 1;
50
51 if (canMakeZeroWithKQueries(mid)) {
52 // If mid queries are sufficient, try fewer
53 right = mid;
54 } else {
55 // If mid queries are not sufficient, need more
56 left = mid + 1;
57 }
58 }
59
60 // If more than available queries needed, return -1
61 return left > queriesLength ? -1 : left;
62}
63
Time and Space Complexity
Time Complexity: O((n + m) × log m)
The algorithm uses binary search on the range [0, m]
where m
is the length of queries. Binary search performs O(log m)
iterations.
For each binary search iteration, the check
function is called, which:
- Iterates through
k
queries (at mostm
queries) to build the difference array:O(m)
- Iterates through
nums
array to verify the condition using the difference array:O(n)
Therefore, each check
call takes O(n + m)
time, and with O(log m)
binary search iterations, the total time complexity is O((n + m) × log m)
.
Space Complexity: O(n)
The space complexity is determined by:
- The difference array
d
of sizen + 1
:O(n)
- Other variables like
s
,l
,k
use constant space:O(1)
The total space complexity is O(n)
, where n
is the length of the nums
array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding Query Operations as Additions Instead of Subtractions
The problem states that queries allow you to subtract values from elements to reduce them to zero, but the solution code treats queries as additions. This is a critical conceptual error that would lead to completely incorrect results.
The Pitfall:
- The code adds
value
to the difference array:diff_array[left] += value
- Then checks if
original_value > cumulative_sum
- This logic assumes we're adding to elements and checking if additions exceed original values
The Correct Understanding:
- Queries provide "reduction capacity" - they allow subtracting from elements
- We need to check if the total reduction capacity at each position is sufficient to reduce that element to zero
Solution:
def can_make_zero(num_queries: int) -> bool:
# Track maximum reduction available at each position
reduction_capacity = [0] * (len(nums) + 1)
# Each query provides reduction capacity in its range
for left, right, value in queries[:num_queries]:
reduction_capacity[left] += value # Start of reduction range
reduction_capacity[right + 1] -= value # End of reduction range
# Check if reduction capacity is sufficient at each position
current_capacity = 0
for i, original_value in enumerate(nums):
current_capacity += reduction_capacity[i]
# Need enough capacity to reduce original value to zero
if original_value > current_capacity:
return False
return True
2. Off-by-One Error in Binary Search Range
The Pitfall:
Using bisect_left(range(total_queries + 1), ...)
is correct, but developers often mistakenly use range(total_queries)
, forgetting that we might need all queries.
Why This Matters:
- If exactly
total_queries
queries are needed,range(total_queries)
would miss this valid answer - The search space should include the possibility of using 0 to
total_queries
queries (inclusive)
3. Incorrect Difference Array Boundary Handling
The Pitfall:
Forgetting to allocate n + 1
elements for the difference array, or incorrectly handling the right + 1
index.
Common Mistake:
# Wrong: This would cause IndexError when right == n-1
diff_array = [0] * len(nums) # Should be len(nums) + 1
diff_array[right + 1] -= value # Would fail for last element
Correct Approach:
Always allocate one extra element to safely handle the range end marker at right + 1
.
Which of the following is a good use case for backtracking?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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!