2870. Minimum Numbers of Operations to Make Array Empty - Explanation

Problem Link

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: 4

Explanation: 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: -1

Explanation: It is impossible to empty the array.

Constraints:

  • 2 <= nums.length <= 100,000
  • 1 <= nums[i] <= 1,000,000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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

  1. Count the frequency of each element in the array.
  2. For each unique element's count, use recursion to find the minimum operations:
    • Base case: if count is 0, return 0. If count is negative, return infinity.
    • Try both dfs(count - 2) and dfs(count - 3), take the min, and add 1.
  3. If any count returns infinity, return -1 (impossible to empty).
  4. 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 res

Time & Space Complexity

  • Time complexity: O(n2m)O(n * 2 ^ m)
  • Space complexity: O(n+m)O(n + m)

Where nn is the size of the array numsnums and mm 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

  1. Count the frequency of each element.
  2. Use memoization (cache) to cache results for each count value.
  3. For each count, if it equals 2 or 3, return 1 (one operation suffices).
  4. Otherwise, recursively compute min(dfs(count - 2), dfs(count - 3)) + 1 and cache it.
  5. Sum the results for all frequencies. Return -1 if 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 res

Time & Space Complexity

  • Time complexity: O(n+m)O(n + m)
  • Space complexity: O(n+m)O(n + m)

Where nn is the size of the array numsnums and mm 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

  1. Count the frequency of each element and find the maxf (maximum frequency).
  2. Create an array minOps where minOps[i] stores the minimum operations to reduce count i to zero.
  3. Set minOps[0] = 0 and minOps[1] = infinity (count of 1 is impossible).
  4. For each count from 2 to maxf, compute minOps[i] = min(minOps[i-2], minOps[i-3]) + 1.
  5. Look up each element's frequency in minOps and sum the results. Return -1 if 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 res

Time & Space Complexity

  • Time complexity: O(n+m)O(n + m)
  • Space complexity: O(n+m)O(n + m)

Where nn is the size of the array numsnums and mm 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

  1. Count the frequency of each element.
  2. For each frequency:
    • If it equals 1, return -1 (impossible).
    • Otherwise, add ceil(count / 3) to the res.
  3. Return the total operations.
class Solution:
    def minOperations(self, nums: List[int]) -> int:
        count = Counter(nums)
        res = 0

        for num, cnt in count.items():
            if cnt == 1:
                return -1
            res += math.ceil(cnt / 3)

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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.