1815. Maximum Number of Groups Getting Fresh Donuts
Problem Description
You have a donut shop that bakes donuts in batches of size batchSize
. The shop has a strict rule: all donuts from one batch must be served before any donuts from the next batch can be served.
Multiple groups of customers will visit your shop, where groups[i]
represents the number of customers in the i-th group. Each customer wants exactly one donut.
When a group arrives at the shop, all customers in that group must be served before any customers from the next group. A group is considered "happy" if they receive fresh donuts - meaning the first customer of their group doesn't get a donut that was left over from serving the previous group.
For example, if batchSize = 4
and a group of 3 customers is served, there will be 1 leftover donut. If the next group arrives, they would get that 1 leftover donut first, making them unhappy since they didn't get all fresh donuts.
You can rearrange the order in which groups visit the shop to maximize happiness. Your goal is to find the maximum number of groups that can be happy.
Key points:
- Groups must be served completely before the next group
- A group is happy only if they start with a fresh batch (no leftovers from previous group)
- You can rearrange the visiting order of groups
- Return the maximum possible number of happy groups
Intuition
The key insight is that a group is happy when they start with no leftover donuts. This happens when the total number of donuts served to all previous groups is a multiple of batchSize
.
Think about it this way: if we've served exactly 12
donuts and batchSize = 4
, we've used exactly 3
complete batches with no leftovers. The next group will be happy because they'll get fresh donuts.
This means we care about the remainder when dividing the cumulative sum of customers by batchSize
. A group starting at position i
is happy if the sum of all group sizes before position i
modulo batchSize
equals 0
.
We can make several observations:
-
Groups with size divisible by
batchSize
: These groups are special. If a group has exactlybatchSize
customers (or any multiple), they consume complete batches and leave no leftovers. We can greedily place all such groups first - they'll all be happy and won't affect subsequent groups. -
Remainder matters, not actual size: For groups whose size isn't divisible by
batchSize
, only the remainder matters. A group of7
customers withbatchSize = 4
has the same effect as a group of3
customers - both leave3
donuts for the next batch. -
The problem becomes arrangement of remainders: After handling complete batches, we need to arrange groups based on their remainders
[1, 2, ..., batchSize-1]
to maximize happy groups. The first group in any arrangement is always happy (starts fresh), and subsequent groups are happy when the cumulative remainder equals0
. -
State representation: Since we only care about how many groups have each possible remainder value, we can represent the state as a frequency count of remainders. With at most
30
groups and remainder values from1
tobatchSize-1
, we can encode this state efficiently using bit manipulation where every5
bits represent the count of one remainder value.
The solution uses dynamic programming with memoization to try different arrangements of these remainder groups, tracking the running remainder to determine when a new group would be happy.
Learn more about Memoization, Dynamic Programming and Bitmask patterns.
Solution Approach
The solution uses greedy strategy combined with state compression and memoized search (dynamic programming).
Step 1: Handle Groups Divisible by BatchSize
First, we identify groups whose size is already a multiple of batchSize
. These groups consume complete batches and leave no leftovers, so they can all be placed at the beginning and will all be happy:
ans = 0 for v in groups: i = v % batchSize ans += i == 0 # Count groups with remainder 0
Step 2: State Compression for Remaining Groups
For groups not divisible by batchSize
, we only care about their remainder when divided by batchSize
. We encode the frequency of each remainder into a single integer state
:
- Use
5
bits to represent the count of each remainder value - Remainder
i
is stored at bit positions[i*5, i*5+4]
- Maximum
batchSize
is9
, so we need at most8 * 5 = 40
bits
state = 0 for v in groups: i = v % batchSize if i: # If remainder is not 0 state += 1 << (i * 5) # Increment count for remainder i
Step 3: Memoized Search with DFS
The core algorithm uses a recursive function dfs(state, mod)
where:
state
: Current frequency distribution of remaindersmod
: Current cumulative remainder (sum of served groups modbatchSize
)- Returns: Maximum number of happy groups possible from this state
@cache
def dfs(state, mod):
res = 0
x = int(mod == 0) # 1 if current group will be happy, 0 otherwise
for i in range(1, batchSize):
if state >> (i * 5) & 31: # Check if remainder i exists
# Try using a group with remainder i
t = dfs(state - (1 << (i * 5)), (mod + i) % batchSize)
res = max(res, t + x)
return res
How the DFS works:
- If
mod == 0
, the next group to be served will be happy (gets fresh donuts), sox = 1
- Try each possible remainder
i
from1
tobatchSize-1
:- Check if we have groups with remainder
i
:(state >> (i * 5)) & 31
extracts the 5-bit count - If available, remove one group with remainder
i
from state:state - (1 << (i * 5))
- Update cumulative remainder:
(mod + i) % batchSize
- Recursively solve the subproblem
- Add
x
to account for whether the current group is happy
- Check if we have groups with remainder
- Return the maximum happiness achievable
Step 4: Final Answer
The total answer combines:
- Groups with size divisible by
batchSize
(all happy) - Optimal arrangement of remaining groups found by
dfs(state, 0)
return ans + dfs(state, 0)
The @cache
decorator provides memoization, preventing redundant calculations for the same (state, mod)
pairs, which significantly improves performance.
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 batchSize = 4
and groups = [1, 3, 2, 5, 2, 2, 1, 6]
.
Step 1: Identify and handle groups divisible by batchSize
First, we check which groups have sizes divisible by 4:
- Group of 1:
1 % 4 = 1
(remainder 1) - Group of 3:
3 % 4 = 3
(remainder 3) - Group of 2:
2 % 4 = 2
(remainder 2) - Group of 5:
5 % 4 = 1
(remainder 1) - Group of 2:
2 % 4 = 2
(remainder 2) - Group of 2:
2 % 4 = 2
(remainder 2) - Group of 1:
1 % 4 = 1
(remainder 1) - Group of 6:
6 % 4 = 2
(remainder 2)
No groups are divisible by 4, so ans = 0
.
Step 2: Build state representation
Count the frequency of each remainder:
- Remainder 1: 3 groups (sizes 1, 5, 1)
- Remainder 2: 4 groups (sizes 2, 2, 2, 6)
- Remainder 3: 1 group (size 3)
Encode this into state using 5 bits per remainder:
- Remainder 1 at bits [5-9]:
3 << 5 = 96
- Remainder 2 at bits [10-14]:
4 << 10 = 4096
- Remainder 3 at bits [15-19]:
1 << 15 = 32768
- Total state:
96 + 4096 + 32768 = 36960
Step 3: Execute DFS search
Start with dfs(36960, 0)
:
Since mod = 0
, the first group we place will be happy (x = 1
).
We try different first groups:
Option A: Start with remainder 1
- Remove one group with remainder 1: new state =
36960 - 32 = 36928
- New mod =
(0 + 1) % 4 = 1
- Recursively solve
dfs(36928, 1)
- In this recursive call,
mod = 1
, so next group won't be happy (x = 0
)
Option B: Start with remainder 2
- Remove one group with remainder 2: new state =
36960 - 1024 = 35936
- New mod =
(0 + 2) % 4 = 2
- Recursively solve
dfs(35936, 2)
Option C: Start with remainder 3
- Remove one group with remainder 3: new state =
36960 - 32768 = 4192
- New mod =
(0 + 3) % 4 = 3
- Recursively solve
dfs(4192, 3)
Let's trace one promising path: Start with remainder 3, then remainder 1:
- Place group with remainder 3:
mod = 3
, this group is happy - Place group with remainder 1:
mod = (3 + 1) % 4 = 0
, this group is happy - Place group with remainder 2:
mod = (0 + 2) % 4 = 2
, this group is happy - Place group with remainder 2:
mod = (2 + 2) % 4 = 0
, this group is not happy - Continue placing remaining groups...
The algorithm explores all possible arrangements. One optimal arrangement might be:
[3] → [1] → [2, 2] → [1, 1, 2, 2]
Where groups in brackets are served together to reach mod = 0
:
- Group with remainder 3: happy (starts fresh)
- Group with remainder 1: happy (cumulative = 4, mod = 0)
- First group with remainder 2: happy (starts fresh after mod = 0)
- Second group with remainder 2: not happy (cumulative = 2)
- Continue analysis...
Step 4: Return final answer
The DFS will find that the maximum number of happy groups is 4, achieved by carefully arranging the groups so that the cumulative remainder becomes 0 as often as possible.
Final answer: 0 + 4 = 4
happy groups.
Solution Implementation
1class Solution:
2 def maxHappyGroups(self, batchSize: int, groups: List[int]) -> int:
3 from functools import cache
4 from typing import List
5
6 @cache
7 def dfs(compressed_state: int, current_remainder: int) -> int:
8 """
9 Dynamic programming function to find maximum happy groups.
10
11 Args:
12 compressed_state: Bit-packed representation of remaining groups
13 current_remainder: Current remainder from previous groups
14
15 Returns:
16 Maximum number of happy groups achievable from this state
17 """
18 max_happy_groups = 0
19
20 # If current remainder is 0, the next group will be happy
21 bonus_if_happy = int(current_remainder == 0)
22
23 # Try using each possible remainder group (1 to batchSize-1)
24 for remainder in range(1, batchSize):
25 # Extract count of groups with this remainder from compressed state
26 # Each remainder uses 5 bits in the state
27 bit_position = remainder * 5
28 count_of_remainder = (compressed_state >> bit_position) & 31
29
30 if count_of_remainder > 0:
31 # Remove one group with this remainder from state
32 new_state = compressed_state - (1 << bit_position)
33
34 # Calculate new remainder after adding this group
35 new_remainder = (current_remainder + remainder) % batchSize
36
37 # Recursively solve for remaining groups
38 happy_from_remaining = dfs(new_state, new_remainder)
39
40 # Update maximum happy groups
41 max_happy_groups = max(max_happy_groups,
42 happy_from_remaining + bonus_if_happy)
43
44 return max_happy_groups
45
46 # Initialize compressed state and count groups divisible by batchSize
47 compressed_state = 0
48 happy_groups_count = 0
49
50 # Process each group
51 for group_size in groups:
52 remainder = group_size % batchSize
53
54 # Groups divisible by batchSize are always happy
55 if remainder == 0:
56 happy_groups_count += 1
57 else:
58 # Pack non-zero remainder groups into compressed state
59 # Each remainder gets 5 bits (can store count up to 31)
60 compressed_state += 1 << (remainder * 5)
61
62 # Add maximum happy groups from non-zero remainder groups
63 happy_groups_count += dfs(compressed_state, 0)
64
65 return happy_groups_count
66
1class Solution {
2 // Memoization cache to store computed states
3 private Map<Long, Integer> memo = new HashMap<>();
4 private int batchSize;
5
6 /**
7 * Find the maximum number of happy groups that can be formed.
8 * A group is happy if they receive fresh donuts (remainder is 0 when they arrive).
9 *
10 * @param batchSize The number of donuts baked at a time
11 * @param groups Array representing the size of each group
12 * @return Maximum number of happy groups
13 */
14 public int maxHappyGroups(int batchSize, int[] groups) {
15 this.batchSize = batchSize;
16 int happyGroups = 0;
17 long state = 0;
18
19 // Process each group
20 for (int groupSize : groups) {
21 int remainder = groupSize % batchSize;
22
23 if (remainder == 0) {
24 // Groups with size divisible by batchSize are always happy
25 happyGroups++;
26 } else {
27 // Encode the count of groups with each remainder into state
28 // Using 5 bits per remainder value (supports up to 31 groups per remainder)
29 state += 1L << (remainder * 5);
30 }
31 }
32
33 // Use DFS to find optimal arrangement for remaining groups
34 happyGroups += dfs(state, 0);
35 return happyGroups;
36 }
37
38 /**
39 * Depth-first search to find the maximum number of happy groups
40 * from the remaining groups encoded in the state.
41 *
42 * @param state Encoded representation of remaining groups (5 bits per remainder)
43 * @param currentRemainder The current remainder of donuts when a group arrives
44 * @return Maximum number of happy groups that can be formed from this state
45 */
46 private int dfs(long state, int currentRemainder) {
47 // Check memoization cache
48 if (memo.containsKey(state)) {
49 return memo.get(state);
50 }
51
52 int maxHappyGroups = 0;
53
54 // Try placing each available group next
55 for (int remainder = 1; remainder < batchSize; remainder++) {
56 // Extract count of groups with this remainder (5 bits)
57 int groupCount = (int)((state >> (remainder * 5)) & 31);
58
59 if (groupCount > 0) {
60 // Remove one group with this remainder from state
61 long nextState = state - (1L << (remainder * 5));
62
63 // Calculate new remainder after this group takes donuts
64 int nextRemainder = (currentRemainder + remainder) % batchSize;
65
66 // Recursively solve for remaining groups
67 int happyGroupsFromNext = dfs(nextState, nextRemainder);
68
69 // If current group arrives to find remainder 0, they are happy
70 int currentGroupHappiness = (currentRemainder == 0) ? 1 : 0;
71
72 // Update maximum happy groups
73 maxHappyGroups = Math.max(maxHappyGroups,
74 happyGroupsFromNext + currentGroupHappiness);
75 }
76 }
77
78 // Cache and return result
79 memo.put(state, maxHappyGroups);
80 return maxHappyGroups;
81 }
82}
83
1class Solution {
2public:
3 int maxHappyGroups(int batchSize, vector<int>& groups) {
4 using ll = long long;
5
6 // Memoization map: stores the maximum happy groups for each state
7 unordered_map<ll, int> memo;
8
9 // Encode the count of each remainder into a single long long value
10 // Each remainder uses 5 bits (can store up to 31 occurrences)
11 ll encodedState = 0;
12
13 // Count groups that are already happy (divisible by batchSize)
14 int happyGroupsCount = 0;
15
16 // Process each group and build the initial state
17 for (auto& groupSize : groups) {
18 int remainder = groupSize % batchSize;
19
20 // Groups with remainder 0 are always happy
21 if (remainder == 0) {
22 happyGroupsCount++;
23 } else {
24 // Encode non-zero remainders into state
25 // Shift by (remainder * 5) bits and increment count
26 encodedState += 1ll << (remainder * 5);
27 }
28 }
29
30 // DFS function to find maximum happy groups from current state
31 // Parameters:
32 // currentState: encoded counts of remaining groups
33 // currentRemainder: remainder from previous groups served
34 function<int(ll, int)> dfs = [&](ll currentState, int currentRemainder) {
35 // Check if this state has been computed before
36 if (memo.count(currentState)) {
37 return memo[currentState];
38 }
39
40 int maxHappyGroups = 0;
41
42 // Current group is happy if previous groups left no remainder
43 int isCurrentGroupHappy = (currentRemainder == 0) ? 1 : 0;
44
45 // Try serving each possible remainder group
46 for (int remainder = 1; remainder < batchSize; ++remainder) {
47 // Extract count of groups with this remainder (5 bits per remainder)
48 int remainderCount = (currentState >> (remainder * 5)) & 31;
49
50 if (remainderCount > 0) {
51 // Remove one group with this remainder from state
52 ll nextState = currentState - (1ll << (remainder * 5));
53
54 // Calculate new remainder after serving this group
55 int nextRemainder = (currentRemainder + remainder) % batchSize;
56
57 // Recursively find best solution from next state
58 int happyGroupsFromNextState = dfs(nextState, nextRemainder);
59
60 // Update maximum if this choice is better
61 maxHappyGroups = max(maxHappyGroups, happyGroupsFromNextState + isCurrentGroupHappy);
62 }
63 }
64
65 // Memoize and return the result
66 return memo[currentState] = maxHappyGroups;
67 };
68
69 // Add groups with remainder 0 and optimal arrangement of other groups
70 happyGroupsCount += dfs(encodedState, 0);
71
72 return happyGroupsCount;
73 }
74};
75
1function maxHappyGroups(batchSize: number, groups: number[]): number {
2 // Memoization map: stores the maximum happy groups for each state
3 const memo = new Map<bigint, number>();
4
5 // Encode the count of each remainder into a single bigint value
6 // Each remainder uses 5 bits (can store up to 31 occurrences)
7 let encodedState = 0n;
8
9 // Count groups that are already happy (divisible by batchSize)
10 let happyGroupsCount = 0;
11
12 // Process each group and build the initial state
13 for (const groupSize of groups) {
14 const remainder = groupSize % batchSize;
15
16 // Groups with remainder 0 are always happy
17 if (remainder === 0) {
18 happyGroupsCount++;
19 } else {
20 // Encode non-zero remainders into state
21 // Shift by (remainder * 5) bits and increment count
22 encodedState += 1n << BigInt(remainder * 5);
23 }
24 }
25
26 // DFS function to find maximum happy groups from current state
27 // Parameters:
28 // currentState: encoded counts of remaining groups
29 // currentRemainder: remainder from previous groups served
30 const dfs = (currentState: bigint, currentRemainder: number): number => {
31 // Check if this state has been computed before
32 if (memo.has(currentState)) {
33 return memo.get(currentState)!;
34 }
35
36 let maxHappyGroups = 0;
37
38 // Current group is happy if previous groups left no remainder
39 const isCurrentGroupHappy = currentRemainder === 0 ? 1 : 0;
40
41 // Try serving each possible remainder group
42 for (let remainder = 1; remainder < batchSize; remainder++) {
43 // Extract count of groups with this remainder (5 bits per remainder)
44 const remainderCount = Number((currentState >> BigInt(remainder * 5)) & 31n);
45
46 if (remainderCount > 0) {
47 // Remove one group with this remainder from state
48 const nextState = currentState - (1n << BigInt(remainder * 5));
49
50 // Calculate new remainder after serving this group
51 const nextRemainder = (currentRemainder + remainder) % batchSize;
52
53 // Recursively find best solution from next state
54 const happyGroupsFromNextState = dfs(nextState, nextRemainder);
55
56 // Update maximum if this choice is better
57 maxHappyGroups = Math.max(maxHappyGroups, happyGroupsFromNextState + isCurrentGroupHappy);
58 }
59 }
60
61 // Memoize and return the result
62 memo.set(currentState, maxHappyGroups);
63 return maxHappyGroups;
64 };
65
66 // Add groups with remainder 0 and optimal arrangement of other groups
67 happyGroupsCount += dfs(encodedState, 0);
68
69 return happyGroupsCount;
70}
71
Time and Space Complexity
Time Complexity: O(batchSize * 31^(batchSize-1))
The analysis breaks down as follows:
- The state is represented using 5 bits for each remainder value from 1 to
batchSize-1
, allowing up to 31 occurrences of each remainder - The total number of possible states is at most
31^(batchSize-1)
since we havebatchSize-1
different remainder values (excluding 0) - For each state, the DFS function iterates through
batchSize-1
possible remainders - Each state is computed only once due to memoization with
@cache
- Therefore, the time complexity is
O(batchSize * 31^(batchSize-1))
For typical constraints where batchSize ≤ 9
, this gives us 31^8 * 9 ≈ 8.5 * 10^11
in the worst case, but in practice, many states are unreachable (since the total number of groups is limited), keeping the actual complexity well below O(10^7)
.
Space Complexity: O(31^(batchSize-1))
The space analysis:
- The memoization cache stores at most
31^(batchSize-1)
different states - Each cached entry stores the state tuple and the result value
- The recursion depth is at most the total number of groups, which is typically much smaller than the number of states
- For
batchSize ≤ 9
, this gives us31^8 ≈ 9.5 * 10^10
theoretical states, but the actual number of valid states encountered is significantly smaller due to the limited number of groups
In practice, with reasonable constraints on the number of groups (typically ≤ 30), the actual space used is much less than the theoretical maximum, keeping it below O(10^6)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect State Representation - Using Lists or Tuples Instead of Bit Compression
Many developers initially try to use a list or tuple to track remainder frequencies:
# WRONG APPROACH - Will cause Time Limit Exceeded
@cache
def dfs(remainder_counts, current_remainder):
remainder_counts = list(remainder_counts) # Convert tuple for modification
# ... rest of logic
Why this fails:
- Python's
@cache
requires hashable arguments - Converting between lists and tuples repeatedly is inefficient
- The state space becomes much larger with tuple representations
Solution: Use bit compression as shown in the correct solution. Each remainder gets 5 bits, allowing efficient state representation in a single integer.
2. Forgetting to Handle Groups Divisible by BatchSize Separately
A common mistake is including groups with remainder 0 in the DFS state:
# WRONG - Including remainder 0 in DFS for group_size in groups: remainder = group_size % batchSize compressed_state += 1 << (remainder * 5) # Includes remainder 0!
Why this fails:
- Groups with remainder 0 always leave the "plate clean" for the next group
- They can all be placed at the beginning and all be happy
- Including them in DFS unnecessarily complicates the state space
Solution: Count groups with remainder 0 separately and add them directly to the answer.
3. Incorrect Bit Manipulation for Extracting/Updating Counts
Developers often make mistakes with bit operations:
# WRONG - Incorrect extraction count = compressed_state >> (remainder * 5) # Missing mask! # WRONG - Incorrect decrement new_state = compressed_state - remainder # Should subtract (1 << bit_position)
Solution:
- Extract count:
(compressed_state >> (remainder * 5)) & 31
- Decrement count:
compressed_state - (1 << (remainder * 5))
4. Not Understanding When a Group is Happy
A critical misunderstanding is when to count a group as happy:
# WRONG - Counting happiness after serving the group
def dfs(state, mod):
for remainder in range(1, batchSize):
if has_remainder(state, remainder):
new_mod = (mod + remainder) % batchSize
# Wrong: checking if new_mod == 0
bonus = int(new_mod == 0) # INCORRECT!
Why this fails:
- A group is happy if they START with fresh donuts (mod == 0 BEFORE serving)
- Not if they FINISH exactly at a batch boundary
Solution: Check mod == 0
at the beginning of each recursive call, before choosing which group to serve next.
5. Off-by-One Error with Remainder Range
# WRONG - Including batchSize as a possible remainder
for remainder in range(1, batchSize + 1): # WRONG!
# Process remainder...
Why this fails:
- Remainders range from 0 to batchSize-1
- A remainder of batchSize would actually be 0
Solution: Use range(1, batchSize)
since remainder 0 is handled separately.
Which data structure is used to implement priority queue?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!