2702. Minimum Operations to Make Numbers Non-positive 🔒
Problem Description
You have an array of integers nums
(0-indexed) and two integer values x
and y
. You can perform operations on this array to reduce all numbers to zero or below.
In each operation, you:
- Choose any index
i
from the array (where0 <= i < nums.length
) - Decrease the value at
nums[i]
byx
- Decrease the values at all other indices (every position except
i
) byy
Your goal is to find the minimum number of operations needed to make every integer in nums
less than or equal to zero.
For example, if you have nums = [10, 5, 8]
, x = 4
, and y = 1
, and you perform one operation choosing index 0:
nums[0]
decreases by 4:10 - 4 = 6
nums[1]
decreases by 1:5 - 1 = 4
nums[2]
decreases by 1:8 - 1 = 7
- Result after operation:
[6, 4, 7]
You would continue performing operations until all values become <= 0
. The problem asks for the minimum number of such operations required.
Intuition
The key observation is that if we can make all numbers <= 0
in t
operations, then we can definitely do it in t+1
operations or any number greater than t
. This monotonic property suggests we can use binary search to find the minimum number of operations.
Let's think about what happens after t
operations:
- Every number gets decreased by
y
in each operation (since it's not chosen), so each number decreases by at leastt * y
- Some numbers might need additional reduction beyond
t * y
to become<= 0
- We can provide this extra reduction by choosing those specific indices during our
t
operations
For a number nums[i]
to become <= 0
after t
operations:
- If
nums[i] <= t * y
, it's already handled (since every position gets reduced byy
in each operation) - If
nums[i] > t * y
, we need extra reduction ofnums[i] - t * y
- Each time we choose index
i
, we get an extra reduction ofx - y
(since we reduce byx
instead ofy
) - So we need to choose index
i
at leastceil((nums[i] - t * y) / (x - y))
times
The crucial insight: if the total number of times we need to choose specific indices (sum of all required selections) is <= t
, then t
operations are sufficient. Otherwise, we need more operations.
This gives us a way to check if a given number of operations t
is feasible, which we can use with binary search to find the minimum valid t
.
Learn more about Binary Search patterns.
Solution Approach
We implement a binary search solution to find the minimum number of operations needed.
Binary Search Setup:
- Initialize the search range:
l = 0
(minimum possible operations) andr = max(nums)
(maximum possible operations needed) - We search for the minimum value of
t
(number of operations) that satisfies our condition
Check Function:
The check(t)
function determines if t
operations are sufficient to make all numbers <= 0
:
-
For each number
v
innums
:- After
t
operations, every number is reduced by at leastt * y
- If
v > t * y
, the remaining amountv - t * y
must be eliminated by choosing this index - When we choose an index, we get extra reduction of
x - y
per operation - So we need
ceil((v - t * y) / (x - y))
operations where this index is chosen
- After
-
Count the total number of times we need to choose specific indices:
cnt = 0 for v in nums: if v > t * y: cnt += ceil((v - t * y) / (x - y))
-
If
cnt <= t
, thent
operations are sufficient (returnTrue
), otherwise we need more operations (returnFalse
)
Binary Search Logic:
while l < r: mid = (l + r) >> 1 # equivalent to (l + r) // 2 if check(mid): r = mid # mid operations are sufficient, try smaller else: l = mid + 1 # mid operations are not enough, need more
The binary search converges when l == r
, giving us the minimum number of operations required. The algorithm efficiently narrows down the search space by halving it in each iteration, resulting in O(n * log(max(nums)))
time complexity, where n
is the length of the 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.
Example: nums = [5, 3]
, x = 3
, y = 1
Step 1: Set up binary search range
l = 0
(minimum possible operations)r = max(nums) = 5
(maximum operations we might need)
Step 2: Binary search iterations
Iteration 1: l = 0, r = 5
mid = (0 + 5) // 2 = 2
- Check if 2 operations are sufficient:
- For
nums[0] = 5
:- After 2 operations, reduced by at least
2 * 1 = 2
- Remaining:
5 - 2 = 3
- Need extra reduction via choosing this index:
ceil(3 / (3-1)) = ceil(3/2) = 2
times
- After 2 operations, reduced by at least
- For
nums[1] = 3
:- After 2 operations, reduced by at least
2 * 1 = 2
- Remaining:
3 - 2 = 1
- Need extra reduction:
ceil(1 / (3-1)) = ceil(1/2) = 1
time
- After 2 operations, reduced by at least
- Total selections needed:
2 + 1 = 3
- Since
3 > 2
, we cannot do it in 2 operations
- For
- Update:
l = mid + 1 = 3
Iteration 2: l = 3, r = 5
mid = (3 + 5) // 2 = 4
- Check if 4 operations are sufficient:
- For
nums[0] = 5
:- After 4 operations, reduced by at least
4 * 1 = 4
- Remaining:
5 - 4 = 1
- Need extra reduction:
ceil(1 / 2) = 1
time
- After 4 operations, reduced by at least
- For
nums[1] = 3
:- After 4 operations, reduced by at least
4 * 1 = 4
- Since
3 <= 4
, already handled (no extra reduction needed)
- After 4 operations, reduced by at least
- Total selections needed:
1 + 0 = 1
- Since
1 <= 4
, we can do it in 4 operations
- For
- Update:
r = mid = 4
Iteration 3: l = 3, r = 4
mid = (3 + 4) // 2 = 3
- Check if 3 operations are sufficient:
- For
nums[0] = 5
:- After 3 operations, reduced by at least
3 * 1 = 3
- Remaining:
5 - 3 = 2
- Need extra reduction:
ceil(2 / 2) = 1
time
- After 3 operations, reduced by at least
- For
nums[1] = 3
:- After 3 operations, reduced by at least
3 * 1 = 3
- Since
3 <= 3
, already handled
- After 3 operations, reduced by at least
- Total selections needed:
1 + 0 = 1
- Since
1 <= 3
, we can do it in 3 operations
- For
- Update:
r = mid = 3
Step 3: Binary search converges
l = r = 3
, so the minimum number of operations is 3
Verification of the answer: Let's verify by simulating 3 operations:
- Operation 1: Choose index 0 →
[5-3, 3-1] = [2, 2]
- Operation 2: Choose index 0 →
[2-3, 2-1] = [-1, 1]
- Operation 3: Choose index 1 →
[-1-1, 1-3] = [-2, -2]
All values are now <= 0
, confirming that 3 operations are indeed sufficient and minimal.
Solution Implementation
1class Solution:
2 def minOperations(self, nums: List[int], x: int, y: int) -> int:
3 """
4 Find minimum operations needed to reduce all elements to 0 or less.
5 Each operation can either:
6 - Reduce all elements by y
7 - Reduce a chosen element by x (where x > y)
8
9 Args:
10 nums: List of integers to reduce
11 x: Amount to reduce a single chosen element
12 y: Amount to reduce all elements
13
14 Returns:
15 Minimum number of operations needed
16 """
17
18 def can_complete_in_time(time: int) -> bool:
19 """
20 Check if all elements can be reduced to 0 or less within given time.
21
22 Strategy: Use 'time' operations total, where some are targeted (reduce by x)
23 and the rest are global (reduce by y).
24
25 Args:
26 time: Number of operations available
27
28 Returns:
29 True if possible within time limit, False otherwise
30 """
31 targeted_operations_needed = 0
32
33 for value in nums:
34 # After 'time' global operations, value becomes (value - time * y)
35 remaining_value = value - time * y
36
37 if remaining_value > 0:
38 # Need targeted operations to handle remaining value
39 # Each targeted operation reduces by (x - y) more than global
40 extra_reduction_needed = remaining_value
41 operations_for_this_element = (extra_reduction_needed + (x - y) - 1) // (x - y) # Ceiling division
42 targeted_operations_needed += operations_for_this_element
43
44 # Check if we have enough operations for all targeted reductions
45 return targeted_operations_needed <= time
46
47 # Binary search for minimum operations needed
48 left, right = 0, max(nums)
49
50 while left < right:
51 mid = (left + right) // 2
52
53 if can_complete_in_time(mid):
54 # If possible with mid operations, try fewer
55 right = mid
56 else:
57 # If not possible with mid operations, need more
58 left = mid + 1
59
60 return left
61
1class Solution {
2 private int[] nums;
3 private int x;
4 private int y;
5
6 /**
7 * Finds the minimum number of operations needed to reduce all elements to 0 or less.
8 * Each operation can either reduce an element by x (chosen element) or y (all other elements).
9 *
10 * @param nums Array of integers to be reduced
11 * @param x Amount to reduce the chosen element in each operation
12 * @param y Amount to reduce all other elements in each operation
13 * @return Minimum number of operations required
14 */
15 public int minOperations(int[] nums, int x, int y) {
16 // Store parameters as instance variables for use in helper method
17 this.nums = nums;
18 this.x = x;
19 this.y = y;
20
21 // Binary search bounds: minimum operations is 0, maximum is the largest element value
22 int left = 0;
23 int right = 0;
24
25 // Find the maximum value in nums array to set upper bound
26 for (int value : nums) {
27 right = Math.max(right, value);
28 }
29
30 // Binary search to find minimum operations needed
31 while (left < right) {
32 // Calculate middle point avoiding overflow
33 int mid = (left + right) >>> 1;
34
35 // Check if 'mid' operations are sufficient
36 if (isValidOperationCount(mid)) {
37 // If valid, try to find a smaller number of operations
38 right = mid;
39 } else {
40 // If not valid, need more operations
41 left = mid + 1;
42 }
43 }
44
45 return left;
46 }
47
48 /**
49 * Checks if the given number of operations is sufficient to reduce all elements.
50 *
51 * @param operationCount Number of operations to validate
52 * @return true if operationCount operations are sufficient, false otherwise
53 */
54 private boolean isValidOperationCount(int operationCount) {
55 long requiredExtraOperations = 0;
56
57 // For each element, calculate how many times it needs to be chosen as target
58 for (int value : nums) {
59 // After 'operationCount' operations, each element is reduced by at least operationCount * y
60 long remainingValue = value - (long) operationCount * y;
61
62 if (remainingValue > 0) {
63 // Calculate additional reductions needed by choosing this element
64 // Each time this element is chosen, it gets reduced by (x - y) more than others
65 // Using ceiling division: (a + b - 1) / b
66 requiredExtraOperations += (remainingValue + x - y - 1) / (x - y);
67 }
68 }
69
70 // Check if required extra operations don't exceed available operations
71 return requiredExtraOperations <= operationCount;
72 }
73}
74
1class Solution {
2public:
3 int minOperations(vector<int>& nums, int x, int y) {
4 // Binary search bounds: minimum 0, maximum is the largest element in nums
5 int left = 0;
6 int right = *max_element(nums.begin(), nums.end());
7
8 // Lambda function to check if we can reduce all numbers to 0 or less in 'time' operations
9 auto canCompleteInTime = [&](int time) {
10 long long operationsNeeded = 0;
11
12 // For each number, calculate how many x-operations are needed
13 for (int num : nums) {
14 // After 'time' operations, each number is reduced by at least time * y
15 // If the remaining value is positive, we need additional x-operations
16 if (num > 1LL * time * y) {
17 // Calculate required x-operations for the remaining value
18 // Using ceiling division: (a + b - 1) / b
19 long long remaining = num - 1LL * time * y;
20 operationsNeeded += (remaining + x - y - 1) / (x - y);
21 }
22 }
23
24 // Check if total x-operations needed doesn't exceed available time
25 return operationsNeeded <= time;
26 };
27
28 // Binary search to find minimum time needed
29 while (left < right) {
30 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
31
32 if (canCompleteInTime(mid)) {
33 // If we can complete in 'mid' time, try to find a smaller value
34 right = mid;
35 } else {
36 // If we cannot complete in 'mid' time, we need more time
37 left = mid + 1;
38 }
39 }
40
41 // Return the minimum number of operations needed
42 return left;
43 }
44};
45
1/**
2 * Finds the minimum number of operations needed to reduce all numbers
3 * @param nums - Array of numbers to be reduced
4 * @param x - Reduction amount for selected element per operation
5 * @param y - Reduction amount for non-selected elements per operation
6 * @returns Minimum number of operations required
7 */
8function minOperations(nums: number[], x: number, y: number): number {
9 // Initialize binary search boundaries
10 let left: number = 0;
11 let right: number = Math.max(...nums);
12
13 /**
14 * Checks if all numbers can be reduced to 0 or below within given operations
15 * @param operations - Number of operations to check
16 * @returns True if possible within the given operations, false otherwise
17 */
18 const canReduceInOperations = (operations: number): boolean => {
19 // Count how many times we need to select each element specifically
20 let selectionsNeeded: number = 0;
21
22 for (const value of nums) {
23 // Calculate remaining value after passive reduction from all operations
24 const remainingAfterPassiveReduction: number = value - operations * y;
25
26 if (remainingAfterPassiveReduction > 0) {
27 // Calculate additional selections needed for this element
28 // Each selection provides extra (x - y) reduction beyond passive reduction
29 const extraReductionPerSelection: number = x - y;
30 selectionsNeeded += Math.ceil(remainingAfterPassiveReduction / extraReductionPerSelection);
31 }
32 }
33
34 // Check if total selections needed doesn't exceed available operations
35 return selectionsNeeded <= operations;
36 };
37
38 // Binary search for minimum operations
39 while (left < right) {
40 const mid: number = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
41
42 if (canReduceInOperations(mid)) {
43 // If possible with mid operations, try fewer
44 right = mid;
45 } else {
46 // If not possible with mid operations, need more
47 left = mid + 1;
48 }
49 }
50
51 return left;
52}
53
Time and Space Complexity
Time Complexity: O(n * log(max(nums)))
The algorithm uses binary search on the range [0, max(nums)]
, which requires O(log(max(nums)))
iterations. In each iteration, the check
function is called, which iterates through all n
elements in the nums
array to calculate the required operations. Each element requires O(1)
time for the arithmetic operations and ceiling division. Therefore, the overall time complexity is O(n * log(max(nums)))
.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space. The variables l
, r
, mid
, t
, cnt
, and v
are all scalar values that don't depend on the input size. The check
function doesn't use any additional data structures that scale with input size. Therefore, the space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division vs Ceiling Division Error
One of the most common mistakes is incorrectly calculating the ceiling division when determining how many targeted operations are needed for each element.
Incorrect Implementation:
operations_for_this_element = remaining_value // (x - y) # Wrong! This is floor division
Why it's wrong: When we have a remaining value that isn't perfectly divisible by (x - y)
, floor division will underestimate the operations needed. For example, if remaining_value = 5
and (x - y) = 2
, we need 3 operations (not 2).
Correct Implementation:
# Method 1: Using math.ceil import math operations_for_this_element = math.ceil(remaining_value / (x - y)) # Method 2: Manual ceiling division (avoiding floating point) operations_for_this_element = (remaining_value + (x - y) - 1) // (x - y)
2. Incorrect Binary Search Boundary
Setting the wrong upper bound for binary search can lead to incorrect results or unnecessary iterations.
Incorrect Implementation:
right = sum(nums) # Wrong! This could be unnecessarily large
Why it's wrong: The maximum operations needed is when we only use global operations (reducing by y
), which would be max(nums) // y + 1
. Using sum(nums)
as the upper bound works but is inefficient.
Correct Implementation:
right = max(nums) # Sufficient upper bound
# Or more precisely:
right = (max(nums) + y - 1) // y # Exact maximum operations needed
3. Edge Case: When x equals y
If x == y
, the problem becomes trivial but the division by (x - y)
would cause a division by zero error.
Solution:
def minOperations(self, nums: List[int], x: int, y: int) -> int:
if x == y:
# All operations have the same effect
return (max(nums) + x - 1) // x
# Continue with binary search approach...
4. Overflow in Ceiling Division Formula
When using the manual ceiling division formula (a + b - 1) // b
, the addition might cause integer overflow in languages with fixed integer sizes.
Solution for Python: Python handles arbitrary precision integers, so this isn't an issue. However, if implementing in other languages:
# Alternative ceiling division without overflow risk operations_for_this_element = remaining_value // (x - y) if remaining_value % (x - y) != 0: operations_for_this_element += 1
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!