2244. Minimum Rounds to Complete All Tasks
Problem Description
You have an array tasks
where each element represents the difficulty level of a task. The array is 0-indexed, meaning positions start from 0.
In each round of work, you can complete either 2 or 3 tasks, but they must all be of the same difficulty level. For example, if you have tasks with difficulty levels [2, 2, 3, 3, 3]
, you could complete 2 tasks of difficulty 2 in one round, and 3 tasks of difficulty 3 in another round.
Your goal is to find the minimum number of rounds needed to complete all tasks. If it's impossible to complete all tasks following these rules, return -1
.
The key constraint is that you can only complete 2 or 3 tasks per round, and all tasks completed in a single round must have the same difficulty level.
For example:
- If you have 6 tasks of the same difficulty, you can complete them in 2 rounds (3 tasks + 3 tasks)
- If you have 4 tasks of the same difficulty, you can complete them in 2 rounds (2 tasks + 2 tasks)
- If you have only 1 task of any difficulty level, it's impossible to complete it (since you must complete at least 2 tasks per round), so you would return
-1
The algorithm needs to determine the optimal way to group tasks of each difficulty level into rounds of 2 or 3 tasks, minimizing the total number of rounds across all difficulty levels.
Intuition
The first observation is that we need to group tasks by their difficulty level since we can only complete tasks of the same difficulty in each round. This naturally leads us to count how many tasks exist for each difficulty level.
For each difficulty level, we need to figure out how to divide the tasks into groups of 2 or 3. Let's think about what numbers can be formed using combinations of 2 and 3:
- 2 = 2 (1 round)
- 3 = 3 (1 round)
- 4 = 2 + 2 (2 rounds)
- 5 = 2 + 3 (2 rounds)
- 6 = 3 + 3 (2 rounds)
- 7 = 2 + 2 + 3 (3 rounds)
- 8 = 2 + 3 + 3 (3 rounds)
- 9 = 3 + 3 + 3 (3 rounds)
Notice that we cannot form 1 using any combination of 2 and 3, so if any difficulty level has exactly 1 task, it's impossible to complete all tasks.
For any number v ≥ 2
, we want to minimize the number of rounds. The key insight is that we should use as many groups of 3 as possible since this reduces the total number of rounds.
When we divide v
by 3, we get three possible remainders:
- If
v % 3 == 0
: We can perfectly divide intov/3
groups of 3 - If
v % 3 == 1
: We have one extra task after grouping by 3s. But wait - we can't have a group of 1! Instead, we take one group of 3 and combine it with the remainder 1 to make two groups of 2. So we need(v/3 - 1) + 2 = v/3 + 1
rounds - If
v % 3 == 2
: We have 2 extra tasks, which forms one additional group. So we needv/3 + 1
rounds
This simplifies to the formula: rounds = v // 3 + (1 if v % 3 != 0 else 0)
, which can be written as v // 3 + (v % 3 != 0)
in Python.
Learn more about Greedy patterns.
Solution Approach
The solution uses a hash table (Counter in Python) to count the frequency of each difficulty level, then applies the mathematical formula we derived to calculate the minimum rounds for each difficulty level.
Step 1: Count Task Frequencies
We use Python's Counter
from the collections module to create a frequency map:
cnt = Counter(tasks)
This gives us a dictionary where keys are difficulty levels and values are the count of tasks for each difficulty.
Step 2: Calculate Rounds for Each Difficulty
We iterate through all the counts in our frequency map:
for v in cnt.values():
For each count v
:
- If
v == 1
, it's impossible to complete these tasks (we need at least 2 tasks per round), so we immediately return-1
- Otherwise, we calculate the minimum rounds needed using the formula:
v // 3 + (v % 3 != 0)
Step 3: Accumulate Total Rounds
The expression v // 3 + (v % 3 != 0)
calculates the minimum rounds for count v
:
v // 3
gives us the base number of rounds if we could use all groups of 3(v % 3 != 0)
adds 1 more round if there's a remainder (evaluates to 1 if true, 0 if false)
This works because:
- When
v % 3 == 0
: We need exactlyv // 3
rounds - When
v % 3 == 1
orv % 3 == 2
: We needv // 3 + 1
rounds
We add the rounds for each difficulty level to our answer:
ans += v // 3 + (v % 3 != 0)
Time Complexity: O(n)
where n is the length of the tasks array, as we iterate through it once to build the counter and then iterate through unique difficulty levels.
Space Complexity: O(k)
where k is the number of unique difficulty levels in the tasks 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 an example with tasks = [2, 2, 3, 3, 2, 4, 4, 4, 4, 4]
.
Step 1: Count Task Frequencies
First, we count how many tasks exist for each difficulty level:
- Difficulty 2: 3 tasks
- Difficulty 3: 2 tasks
- Difficulty 4: 5 tasks
Step 2: Calculate Rounds for Each Difficulty
Now we determine the minimum rounds needed for each difficulty level:
Difficulty 2 (3 tasks):
- 3 divided by 3 gives quotient 1, remainder 0
- Since remainder is 0, we need: 1 + 0 = 1 round
- We complete all 3 tasks in 1 round
Difficulty 3 (2 tasks):
- 2 divided by 3 gives quotient 0, remainder 2
- Since remainder is non-zero, we need: 0 + 1 = 1 round
- We complete both tasks in 1 round
Difficulty 4 (5 tasks):
- 5 divided by 3 gives quotient 1, remainder 2
- Since remainder is non-zero, we need: 1 + 1 = 2 rounds
- We can complete this as: 3 tasks in round 1, 2 tasks in round 2
Step 3: Calculate Total Rounds
Total rounds = 1 (for difficulty 2) + 1 (for difficulty 3) + 2 (for difficulty 4) = 4 rounds
The complete execution would be:
- Round 1: Complete 3 tasks of difficulty 2
- Round 2: Complete 2 tasks of difficulty 3
- Round 3: Complete 3 tasks of difficulty 4
- Round 4: Complete 2 tasks of difficulty 4
If any difficulty had exactly 1 task, we would return -1 since it's impossible to complete a single task when we must complete 2 or 3 tasks per round.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def minimumRounds(self, tasks: List[int]) -> int:
6 # Count the frequency of each task difficulty level
7 task_frequency = Counter(tasks)
8
9 # Initialize total rounds counter
10 total_rounds = 0
11
12 # Process each task difficulty group
13 for count in task_frequency.values():
14 # If only 1 task of this difficulty, impossible to complete
15 # (minimum round size is 2)
16 if count == 1:
17 return -1
18
19 # Calculate minimum rounds needed for this task group
20 # If count is divisible by 3: count // 3 rounds
21 # If count % 3 == 1: (count // 3 - 1) + 2 = count // 3 + 1 rounds
22 # If count % 3 == 2: count // 3 + 1 rounds
23 # This simplifies to: count // 3 + (1 if count % 3 != 0 else 0)
24 total_rounds += count // 3 + (count % 3 != 0)
25
26 return total_rounds
27
1class Solution {
2 public int minimumRounds(int[] tasks) {
3 // Count frequency of each task difficulty level
4 Map<Integer, Integer> taskFrequency = new HashMap<>();
5 for (int task : tasks) {
6 taskFrequency.merge(task, 1, Integer::sum);
7 }
8
9 // Calculate minimum rounds needed
10 int totalRounds = 0;
11 for (int frequency : taskFrequency.values()) {
12 // If frequency is 1, impossible to complete (need groups of 2 or 3)
13 if (frequency == 1) {
14 return -1;
15 }
16
17 // Calculate minimum rounds for this frequency
18 // If divisible by 3: frequency / 3 rounds
19 // If remainder is 1 or 2: (frequency / 3) + 1 rounds
20 // This works because remainder 1 becomes (n-4)/3 + 2 groups (4 = 2+2)
21 // and remainder 2 becomes (n-2)/3 + 1 group (2 = 2)
22 totalRounds += frequency / 3 + (frequency % 3 == 0 ? 0 : 1);
23 }
24
25 return totalRounds;
26 }
27}
28
1class Solution {
2public:
3 int minimumRounds(vector<int>& tasks) {
4 // Count frequency of each task difficulty level
5 unordered_map<int, int> taskFrequency;
6 for (const auto& task : tasks) {
7 ++taskFrequency[task];
8 }
9
10 int totalRounds = 0;
11
12 // Process each task difficulty group
13 for (const auto& [taskDifficulty, count] : taskFrequency) {
14 // If only one task of this difficulty, impossible to complete
15 if (count == 1) {
16 return -1;
17 }
18
19 // Calculate minimum rounds needed for this difficulty
20 // If count is divisible by 3, we need count/3 rounds
21 // If count % 3 == 1, we need (count/3 - 1) + 2 rounds = count/3 + 1
22 // If count % 3 == 2, we need count/3 + 1 rounds
23 // This simplifies to: count/3 + (1 if remainder exists, 0 otherwise)
24 totalRounds += count / 3 + (count % 3 != 0 ? 1 : 0);
25 }
26
27 return totalRounds;
28 }
29};
30
1/**
2 * Calculates the minimum number of rounds needed to complete all tasks
3 * where each round can complete either 2 or 3 tasks of the same difficulty
4 * @param tasks - Array of task difficulties
5 * @returns Minimum rounds needed, or -1 if impossible
6 */
7function minimumRounds(tasks: number[]): number {
8 // Map to store frequency count of each task difficulty
9 const taskFrequencyMap: Map<number, number> = new Map();
10
11 // Count occurrences of each task difficulty
12 for (const taskDifficulty of tasks) {
13 taskFrequencyMap.set(
14 taskDifficulty,
15 (taskFrequencyMap.get(taskDifficulty) || 0) + 1
16 );
17 }
18
19 // Initialize total rounds counter
20 let totalRounds: number = 0;
21
22 // Process each unique task difficulty
23 for (const frequency of taskFrequencyMap.values()) {
24 // Cannot complete if only one task of this difficulty exists
25 if (frequency === 1) {
26 return -1;
27 }
28
29 // Calculate minimum rounds for this difficulty
30 // If divisible by 3: use frequency/3 rounds
31 // If remainder is 1 or 2: use frequency/3 + 1 rounds
32 totalRounds += Math.floor(frequency / 3) + (frequency % 3 === 0 ? 0 : 1);
33 }
34
35 return totalRounds;
36}
37
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the tasks
array. This is because:
- Creating the Counter takes
O(n)
time as it needs to iterate through all elements intasks
- Iterating through the values in the Counter takes
O(k)
time, wherek
is the number of unique tasks, andk ≤ n
- Each operation inside the loop (checking if
v == 1
and calculatingv // 3 + (v % 3 != 0)
) takesO(1)
time - Overall:
O(n) + O(k) × O(1) = O(n)
The space complexity is O(n)
, where n
is the length of the tasks
array. This is because:
- The Counter dictionary stores at most
n
unique task values (in the worst case where all tasks are different) - The space used by the Counter is proportional to the number of unique elements, which is at most
n
- Other variables (
ans
,v
) useO(1)
space - Overall:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling the Count of 1
Pitfall: Forgetting to check if any task difficulty appears only once, or checking this condition after calculating rounds instead of immediately returning -1.
Wrong Implementation:
def minimumRounds(self, tasks: List[int]) -> int:
task_frequency = Counter(tasks)
total_rounds = 0
for count in task_frequency.values():
# Calculating first, then checking - wastes computation
rounds = count // 3 + (count % 3 != 0)
if count == 1:
return -1
total_rounds += rounds
return total_rounds
Correct Approach: Check for count == 1 immediately before any calculations.
2. Misunderstanding the Mathematical Formula
Pitfall: Using incorrect formulas like (count + 2) // 3
or ceil(count / 3)
without understanding why they work.
Wrong Implementation:
# This seems logical but fails for certain cases if count % 3 == 1: total_rounds += count // 3 + 1 elif count % 3 == 2: total_rounds += count // 3 + 1 else: total_rounds += count // 3
While this works, it's unnecessarily verbose. The expression count // 3 + (count % 3 != 0)
elegantly handles all cases.
3. Not Using Efficient Data Structures
Pitfall: Manually counting frequencies instead of using Counter, leading to verbose and error-prone code.
Wrong Implementation:
def minimumRounds(self, tasks: List[int]) -> int:
frequency = {}
for task in tasks:
if task in frequency:
frequency[task] += 1
else:
frequency[task] = 1
# ... rest of the code
Better Approach: Use Counter(tasks)
for cleaner, more readable code.
4. Integer Division vs Float Division
Pitfall: Using float division /
instead of integer division //
, causing type issues and incorrect results.
Wrong Implementation:
# This creates floating point numbers unnecessarily
total_rounds += int(count / 3) + (count % 3 != 0)
Correct Approach: Always use //
for integer division in Python 3.
5. Edge Case: Empty Array
Pitfall: Not considering what happens when the tasks array is empty.
Solution: The current implementation handles this correctly - Counter returns an empty dictionary, the loop doesn't execute, and the function returns 0 (which is correct for zero tasks).
6. Overthinking the Problem
Pitfall: Trying to use dynamic programming or complex algorithms when a simple mathematical solution exists.
Example of Overcomplication:
def minimumRounds(self, tasks: List[int]) -> int:
task_frequency = Counter(tasks)
total = 0
for count in task_frequency.values():
if count == 1:
return -1
# Unnecessary DP for a simple problem
dp = [float('inf')] * (count + 1)
dp[0] = 0
for i in range(2, count + 1):
if i >= 2:
dp[i] = min(dp[i], dp[i-2] + 1)
if i >= 3:
dp[i] = min(dp[i], dp[i-3] + 1)
total += dp[count]
return total
This DP approach works but is overkill for a problem with a simple mathematical solution.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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!