2870. Minimum Number of Operations to Make Array Empty - Explanation
Description
You are given a 0-indexed array nums consisting of positive integers.
There are two types of operations that you can apply on the array any number of times:
- Choose two elements with equal values and delete them from the array.
- Choose three elements with equal values and delete them from the array.
Return the minimum number of operations required to make the array empty, or -1 if it is not possible.
Example 1:
Input: nums = [2,3,3,2,2,4,2,3,4]
Output: 4Explanation: We can apply the following operations to make the array empty:
- Apply the first operation on the elements at indices 0 and 3. The resulting array is nums = [3,3,2,4,2,3,4].
- Apply the first operation on the elements at indices 2 and 4. The resulting array is nums = [3,3,4,3,4].
- Apply the second operation on the elements at indices 0, 1, and 3. The resulting array is nums = [4,4].
- Apply the first operation on the elements at indices 0 and 1. The resulting array is nums = [].
It can be shown that we cannot make the array empty in less than 4 operations.
Example 2:
Input: nums = [2,1,2,2,3,3]
Output: -1Explanation: It is impossible to empty the array.
Constraints:
2 <= nums.length <= 100,0001 <= nums[i] <= 1,000,000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Hash Maps / Frequency Counting - Required to count occurrences of each element in the array
- Recursion - The base approach explores all possible deletion choices recursively
- Dynamic Programming (Memoization) - Used to optimize the recursive solution by caching subproblem results
- Basic Math (Ceiling Division) - The greedy solution relies on understanding ceiling division for optimal grouping
1. Recursion
Intuition
We can only delete elements in groups of 2 or 3, and all elements in a group must be identical. This means we need to count the frequency of each element and figure out how to reduce each frequency to zero using the minimum number of deletions.
For any count, we try both options: subtract 2 or subtract 3, and recursively solve for the remaining count. If we reach exactly 0, we are done. If we go negative, that path is invalid. The min of both branches gives us the answer.
Algorithm
- Count the frequency of each element in the array.
- For each unique element's
count, use recursion to find the minimum operations:- Base case: if
countis0, return0. Ifcountis negative, return infinity. - Try both
dfs(count - 2)anddfs(count - 3), take themin, and add1.
- Base case: if
- If any
countreturns infinity, return-1(impossible to empty). - Sum up the minimum operations for all frequencies.
class Solution:
def minOperations(self, nums: List[int]) -> int:
def dfs(cur):
if cur < 0:
return float('inf')
if cur == 0:
return 0
ops = min(dfs(cur - 2), dfs(cur - 3))
return 1 + ops
count = Counter(nums)
res = 0
for num, cnt in count.items():
op = dfs(cnt)
if op == float("inf"):
return -1
res += op
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array and is the average frequency of the array elements.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems. When computing the minimum operations for a count, we may revisit the same count multiple times. By caching results, we avoid redundant computation and make the solution efficient.
Algorithm
- Count the frequency of each element.
- Use memoization (
cache) to cache results for eachcountvalue. - For each
count, if it equals2or3, return1(one operation suffices). - Otherwise, recursively compute
min(dfs(count - 2), dfs(count - 3)) + 1and cache it. - Sum the results for all frequencies. Return
-1if any frequency is impossible.
class Solution:
def minOperations(self, nums: List[int]) -> int:
cache = {}
def dfs(num):
if num < 0:
return float("inf")
if num in [2, 3]:
return 1
if num in cache:
return cache[num]
res = min(dfs(num - 2), dfs(num - 3))
cache[num] = res + 1
return cache[num]
count = Counter(nums)
res = 0
for num, cnt in count.items():
op = dfs(cnt)
if op == float("inf"):
return -1
res += op
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array and is the average frequency of the array elements.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of solving recursively from larger counts down to zero, we can build up solutions from smaller counts. We precompute the minimum operations for all counts from 0 up to the maximum frequency, using the recurrence relation derived earlier.
Algorithm
- Count the frequency of each element and find the
maxf(maximum frequency). - Create an array
minOpswhereminOps[i]stores the minimum operations to reducecountito zero. - Set
minOps[0] = 0andminOps[1] = infinity(countof1is impossible). - For each
countfrom2tomaxf, computeminOps[i] = min(minOps[i-2], minOps[i-3]) + 1. - Look up each element's frequency in
minOpsand sum the results. Return-1if any is infinity.
class Solution:
def minOperations(self, nums: List[int]) -> int:
count = Counter(nums)
maxf = max(count.values())
minOps = [0] * (maxf + 1)
minOps[1] = float("inf")
for i in range(2, maxf + 1):
minOps[i] = minOps[i - 2]
if i - 3 >= 0:
minOps[i] = min(minOps[i], minOps[i - 3])
if minOps[i] != float("inf"):
minOps[i] += 1
res = 0
for num, cnt in count.items():
op = minOps[cnt]
if op == float("inf"):
return -1
res += op
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array and is the average frequency of the array elements.
4. Greedy
Intuition
There is a mathematical pattern: for any count greater than 1, the minimum operations is the ceiling of count / 3. This works because we prioritize groups of 3, and any remainder can be handled by converting some 3s to 2s. For example, count = 4 uses two 2s, count = 5 uses one 2 and one 3.
The only impossible case is when count equals 1, since we need at least 2 identical elements to perform any deletion.
Algorithm
- Count the frequency of each element.
- For each frequency:
- If it equals
1, return-1(impossible). - Otherwise, add
ceil(count / 3)to theres.
- If it equals
- Return the total operations.
Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Forgetting the Impossible Case
When an element appears exactly once, it is impossible to delete it since the minimum group size is 2. Failing to check for frequencies of 1 and return -1 will produce incorrect results for inputs that cannot be emptied.
Using Division Instead of Ceiling
The greedy solution requires ceiling division (ceil(count / 3)), not floor division. Using count / 3 will undercount operations for counts like 4, 5, 7, or 8 where remainders require additional groups.
Integer Overflow When Adding One
In some languages, when using the formula (count + 2) / 3 for ceiling division, adding 2 to a very large count could cause overflow. While unlikely given typical constraints, be aware of this when working with custom large inputs.
Sign in to join the discussion