Facebook Pixel

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.

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

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] adds val 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 position l (start of range)
  • Subtract val at position r+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.

  1. Initialize Difference Array: Create an array d of length n + 1 filled with zeros. This will track the reduction capacity changes.

  2. Process First k Queries: For each query [l, r, val] in the first k queries:

    • Add val to d[l] (marking the start of the range)
    • Subtract val from d[r + 1] (marking the end of the range)
  3. Compute Actual Reduction Capacity:

    • Initialize a running sum s = 0
    • Iterate through each index i from 0 to n-1
    • Add d[i] to the running sum s
    • At each position, s represents the total reduction capacity available
    • If nums[i] > s, it means we cannot reduce nums[i] to zero with the available capacity, so return False
  4. 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 Evaluator

Example 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] += 2d = [2, 0, 0, 0]
    • d[2] -= 2d = [2, 0, -2, 0]
  • Query 2 [1, 2, 1]:
    • d[1] += 1d = [2, 1, -2, 0]
    • d[3] -= 1d = [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

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] += 1d = [3, 1, -2, -1]
    • d[3] -= 1d = [3, 1, -2, -2]

Computing Reduction Capacity:

  • Index 0: s = 0 + 3 = 3
    • Check: nums[0] = 3 ≤ 3? Yes! Can reduce to 0.
  • Index 1: s = 3 + 1 = 4
    • Check: nums[1] = 2 ≤ 4? Yes! Can reduce to 0.
  • Index 2: s = 4 + (-2) = 2
    • Check: nums[2] = 1 ≤ 2? Yes! Can reduce to 0.
  • 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 most m 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 size n + 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

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

Load More