2870. Minimum Number of Operations to Make Array Empty
Problem Description
You have an array nums
containing positive integers (0-indexed). You can perform two types of operations on this array as many times as needed:
- Delete 2 elements - Pick any two elements that have the same value and remove them from the array
- Delete 3 elements - Pick any three elements that have the same value and remove them from the array
Your goal is to find the minimum number of operations needed to completely empty the array. If it's impossible to empty the array using these operations, return -1
.
For example, if you have the array [2, 2, 3, 3, 3]
:
- You can delete the two 2's using one operation (type 1)
- You can delete the three 3's using one operation (type 2)
- Total: 2 operations to empty the array
The key insight is that for any value appearing c
times:
- If
c = 1
, it's impossible to delete (return-1
) - If
c = 2
, use one type-1 operation - If
c = 3
, use one type-2 operation - If
c = 4
, use two type-1 operations - If
c = 5
, use one type-1 and one type-2 operation - And so on...
The pattern shows that for any count c ≥ 2
, the minimum operations needed is ⌊(c + 2) / 3⌋
. This formula gives us the optimal way to combine operations of deleting 2 or 3 elements to minimize the total number of operations.
Intuition
Let's think about what happens when we have a value that appears c
times. We need to remove all c
occurrences using operations that remove either 2 or 3 elements at a time.
First, if c = 1
, we can't remove it at all since both operations require at least 2 elements. So we return -1
.
For any c ≥ 2
, we want to minimize the number of operations. Let's explore some cases:
c = 2
: Use 1 operation (remove 2)c = 3
: Use 1 operation (remove 3)c = 4
: Use 2 operations (remove 2 + 2)c = 5
: Use 2 operations (remove 2 + 3)c = 6
: Use 2 operations (remove 3 + 3)c = 7
: Use 3 operations (remove 2 + 2 + 3)c = 8
: Use 3 operations (remove 2 + 3 + 3)c = 9
: Use 3 operations (remove 3 + 3 + 3)
Notice a pattern? We're trying to use as many "remove 3" operations as possible since they're more efficient (removing more elements per operation). The key observation is that any number c ≥ 2
can be expressed as a combination of 2s and 3s:
- Numbers divisible by 3 use only 3s
- Numbers leaving remainder 1 when divided by 3 (like 4, 7, 10) use two 2s and the rest as 3s
- Numbers leaving remainder 2 when divided by 3 (like 2, 5, 8) use one 2 and the rest as 3s
This leads us to the formula ⌊(c + 2) / 3⌋
:
- When
c % 3 == 0
: We needc / 3
operations - When
c % 3 == 1
: We need(c - 4) / 3 + 2 = (c + 2) / 3
operations (replace one 3 with two 2s) - When
c % 3 == 2
: We need(c - 2) / 3 + 1 = (c + 2) / 3
operations (add one 2)
The formula elegantly captures all three cases, giving us the minimum number of operations for any count c ≥ 2
.
Learn more about Greedy patterns.
Solution Approach
The implementation uses a hash table (Counter in Python) to count occurrences of each element, then applies the greedy formula to calculate the minimum operations.
Step 1: Count Element Frequencies
count = Counter(nums)
We use Python's Counter
to create a frequency map where keys are the unique elements and values are their occurrence counts.
Step 2: Calculate Operations for Each Unique Element
ans = 0 for c in count.values(): if c == 1: return -1 ans += (c + 2) // 3
For each frequency c
in the hash table:
- If
c == 1
, it's impossible to remove this element, so we immediately return-1
- Otherwise, we add
(c + 2) // 3
operations to our answer
The formula (c + 2) // 3
works because:
- Integer division
//
automatically floors the result - Adding 2 before dividing by 3 gives us the ceiling effect for the specific pattern we need:
- When
c % 3 == 0
:(c + 2) // 3 = c // 3
(exact division) - When
c % 3 == 1
:(c + 2) // 3 = (c + 2) // 3 = (c - 1) // 3 + 1
(need extra operation for remainder) - When
c % 3 == 2
:(c + 2) // 3 = (c + 2) // 3 = c // 3 + 1
(need extra operation for remainder)
- When
Step 3: Return Total Operations
return ans
The sum of operations for all unique elements gives us the minimum total operations needed.
Time Complexity: O(n)
where n
is the length of the input array (for counting frequencies)
Space Complexity: O(k)
where k
is the number of unique elements in 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 the solution with the array nums = [14, 12, 14, 14, 12, 14, 14, 12, 12, 12, 12, 14, 14, 12, 14, 14]
.
Step 1: Count frequencies First, we count how many times each element appears:
- Element 12 appears 7 times
- Element 14 appears 9 times
Step 2: Calculate operations for each unique element
For element 12 (appears 7 times):
- We need to remove all 7 occurrences
- Using the formula:
(7 + 2) // 3 = 9 // 3 = 3
operations - We can verify this: 7 = 3 + 2 + 2 (one "remove 3" and two "remove 2" operations)
For element 14 (appears 9 times):
- We need to remove all 9 occurrences
- Using the formula:
(9 + 2) // 3 = 11 // 3 = 3
operations - We can verify this: 9 = 3 + 3 + 3 (three "remove 3" operations)
Step 3: Sum up total operations Total operations = 3 + 3 = 6
Let's trace through the actual removals to verify:
- Remove three 12's → Array has 4 twelves and 9 fourteens left
- Remove two 12's → Array has 2 twelves and 9 fourteens left
- Remove two 12's → Array has 0 twelves and 9 fourteens left
- Remove three 14's → Array has 0 twelves and 6 fourteens left
- Remove three 14's → Array has 0 twelves and 3 fourteens left
- Remove three 14's → Array is empty
The array is successfully emptied in 6 operations, confirming our calculation.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def minOperations(self, nums: List[int]) -> int:
6 # Count frequency of each number in the array
7 frequency_map = Counter(nums)
8
9 # Initialize total operations counter
10 total_operations = 0
11
12 # Iterate through each frequency value
13 for frequency in frequency_map.values():
14 # If any number appears exactly once, it's impossible to remove all elements
15 # (we can only remove elements in groups of 2 or 3)
16 if frequency == 1:
17 return -1
18
19 # Calculate minimum operations needed for this frequency
20 # The formula (frequency + 2) // 3 gives us the optimal number of operations:
21 # - If frequency % 3 == 0: we need frequency // 3 operations (all groups of 3)
22 # - If frequency % 3 == 1: we need (frequency + 2) // 3 = (frequency - 1) // 3 + 1 operations
23 # (this becomes frequency // 3 - 1 groups of 3, and 2 groups of 2)
24 # - If frequency % 3 == 2: we need (frequency + 2) // 3 = frequency // 3 + 1 operations
25 # (frequency // 3 groups of 3, and 1 group of 2)
26 total_operations += (frequency + 2) // 3
27
28 return total_operations
29
1class Solution {
2 public int minOperations(int[] nums) {
3 // Count frequency of each number in the array
4 Map<Integer, Integer> frequencyMap = new HashMap<>();
5 for (int num : nums) {
6 frequencyMap.merge(num, 1, Integer::sum);
7 }
8
9 int totalOperations = 0;
10
11 // Process each frequency count
12 for (int frequency : frequencyMap.values()) {
13 // If frequency is 1, impossible to remove (need at least 2 elements)
14 if (frequency < 2) {
15 return -1;
16 }
17
18 // Calculate minimum operations needed for this frequency
19 // Strategy: Use as many groups of 3 as possible, handle remainder
20 int remainder = frequency % 3;
21 int quotient = frequency / 3;
22
23 // If remainder is 0: use quotient groups of 3
24 // If remainder is 1: use (quotient - 1) groups of 3 and 2 groups of 2
25 // If remainder is 2: use quotient groups of 3 and 1 group of 2
26 if (remainder == 0) {
27 totalOperations += quotient;
28 } else {
29 totalOperations += quotient + 1;
30 }
31 }
32
33 return totalOperations;
34 }
35}
36
1class Solution {
2public:
3 int minOperations(vector<int>& nums) {
4 // Count frequency of each number in the array
5 unordered_map<int, int> frequencyMap;
6 for (int number : nums) {
7 ++frequencyMap[number];
8 }
9
10 int totalOperations = 0;
11
12 // Process each unique number and its frequency
13 for (const auto& [value, frequency] : frequencyMap) {
14 // If frequency is 1, it's impossible to form groups of 2 or 3
15 if (frequency < 2) {
16 return -1;
17 }
18
19 // Calculate minimum operations needed for this frequency
20 // This formula gives optimal grouping into 2s and 3s:
21 // - If frequency % 3 == 0: use all 3s (frequency / 3 operations)
22 // - If frequency % 3 == 1: use (frequency / 3 - 1) 3s and two 2s ((frequency + 2) / 3 operations)
23 // - If frequency % 3 == 2: use (frequency / 3) 3s and one 2 ((frequency + 2) / 3 operations)
24 totalOperations += (frequency + 2) / 3;
25 }
26
27 return totalOperations;
28 }
29};
30
1/**
2 * Calculates the minimum number of operations to make all elements disappear from the array.
3 * Each operation can remove either 2 or 3 equal elements.
4 *
5 * @param nums - The input array of numbers
6 * @returns The minimum number of operations, or -1 if impossible
7 */
8function minOperations(nums: number[]): number {
9 // Create a frequency map to count occurrences of each number
10 const frequencyMap: Map<number, number> = new Map();
11
12 // Count the frequency of each number in the array
13 for (const num of nums) {
14 frequencyMap.set(num, (frequencyMap.get(num) ?? 0) + 1);
15 }
16
17 // Initialize the total operations counter
18 let totalOperations: number = 0;
19
20 // Iterate through each frequency count
21 for (const [_, frequency] of frequencyMap) {
22 // If any number appears only once, it's impossible to remove it
23 // (we need at least 2 equal elements for an operation)
24 if (frequency < 2) {
25 return -1;
26 }
27
28 // Calculate minimum operations needed for this frequency
29 // Using ceiling division: Math.ceil(frequency / 3)
30 // This works because we want to use as many 3-removals as possible
31 // and use 2-removals for the remainder
32 totalOperations += Math.floor((frequency + 2) / 3);
33 }
34
35 return totalOperations;
36}
37
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. This is because:
- Creating the Counter object requires iterating through all
n
elements once:O(n)
- Iterating through the values in the Counter dictionary takes
O(k)
time, wherek
is the number of unique elements, andk ≤ n
- Each operation inside the loop (checking if
c == 1
and calculating(c + 2) // 3
) takesO(1)
time - Overall:
O(n) + O(k) = O(n)
The space complexity is O(n)
. This is because:
- The Counter object stores at most
n
key-value pairs (in the worst case where all elements are unique) - The variable
ans
usesO(1)
space - Overall:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Single Occurrences
One of the most common mistakes is forgetting to check if any element appears exactly once in the array. Since we can only remove elements in groups of 2 or 3, a single occurrence makes it impossible to empty the array.
Incorrect Implementation:
def minOperations(self, nums: List[int]) -> int:
frequency_map = Counter(nums)
total_operations = 0
for frequency in frequency_map.values():
# Missing the check for frequency == 1
total_operations += (frequency + 2) // 3
return total_operations
Correct Implementation:
def minOperations(self, nums: List[int]) -> int:
frequency_map = Counter(nums)
total_operations = 0
for frequency in frequency_map.values():
if frequency == 1: # Critical check
return -1
total_operations += (frequency + 2) // 3
return total_operations
2. Using the Wrong Formula for Minimum Operations
Another pitfall is trying to use a different formula or complex conditional logic instead of the elegant (frequency + 2) // 3
formula.
Incorrect/Overcomplicated Implementation:
def minOperations(self, nums: List[int]) -> int:
frequency_map = Counter(nums)
total_operations = 0
for frequency in frequency_map.values():
if frequency == 1:
return -1
elif frequency % 3 == 0:
total_operations += frequency // 3
elif frequency % 3 == 1:
total_operations += (frequency - 4) // 3 + 2 # Overly complex
else: # frequency % 3 == 2
total_operations += frequency // 3 + 1
return total_operations
Correct Implementation:
def minOperations(self, nums: List[int]) -> int:
frequency_map = Counter(nums)
total_operations = 0
for frequency in frequency_map.values():
if frequency == 1:
return -1
total_operations += (frequency + 2) // 3 # Simple, unified formula
return total_operations
3. Attempting to Greedily Remove Elements Instead of Working with Frequencies
Some might try to actually simulate removing elements from the array, which is unnecessary and inefficient.
Incorrect Approach:
def minOperations(self, nums: List[int]) -> int:
nums_copy = nums.copy()
operations = 0
while nums_copy:
# Try to find and remove groups of 3 or 2
# This approach is inefficient and error-prone
# ...complex logic to actually remove elements...
return operations
Correct Approach: Work with frequencies directly without modifying the array, as shown in the correct solution above.
4. Integer Division vs Float Division
Using regular division /
instead of integer division //
will give incorrect results.
Incorrect:
total_operations += (frequency + 2) / 3 # Returns float, not int
Correct:
total_operations += (frequency + 2) // 3 # Returns int
In a binary min heap, the maximum element can be found in:
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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!