Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1815. Maximum Number of Groups Getting Fresh Donuts - Leetcode Solution

Problem Description

Given an integer batchSize and an array groups where groups[i] is the number of people in the i-th group waiting for donuts, you are to distribute donuts in batches of size batchSize. Each batch is made fresh and can serve up to batchSize people. Groups are served in order, and you can only start a new batch if the previous one is finished.

A group is considered "happy" if the first person in the group gets a fresh donut (i.e., the batch is empty when they arrive). Your goal is to reorder the groups (in any order) to maximize the number of happy groups.

Constraints:

  • 1 ≤ batchSize ≤ 9
  • 1 ≤ groups.length ≤ 30
  • 1 ≤ groups[i] ≤ 109
Find the maximum number of happy groups. Each group must be used exactly once.

Thought Process

At first glance, the problem seems to require checking all permutations of the group order to maximize the number of happy groups. However, with up to 30 groups, trying all permutations is infeasible (30! is enormous).

The key insight is that a group is happy if it arrives when the batch is empty, i.e., the current donut count modulo batchSize is zero. The order of groups can be rearranged, so we can optimize the sequence.

Instead of thinking about the actual values in groups, we can focus on the remainders when each group's size is divided by batchSize. This is because only the remainder affects the batch status.

The challenge is to efficiently explore possible groupings and orderings without brute-forcing all permutations, possibly using memoization or dynamic programming to avoid redundant calculations.

Solution Approach

To solve this problem efficiently, we use a dynamic programming approach with memoization. Here’s the step-by-step breakdown:

  1. Reduce Groups to Remainders: For each group, compute group % batchSize. This tells us how much of the current batch they will "use up" and is all that matters for subsequent calculations.
  2. Count Remainder Frequencies: For each possible remainder (from 1 to batchSize - 1), count how many groups have that remainder.
  3. Happy Groups from Zero Remainder: Any group with a remainder of 0 always starts a fresh batch and is always happy. We can count these immediately.
  4. State Representation for DP: Since the order of groups matters only in terms of remainders left, we can represent the state as a tuple of counts of each remainder. This is small enough to memoize.
  5. Recursive Search with Memoization: Use depth-first search (DFS) with memoization. At each state, try using a group with a certain remainder, update the counts, and recursively solve for the new state. Track the curr value (how many donuts are left in the current batch).
  6. Transition: For each possible remainder, if there is at least one group with that remainder left, try taking that group next. If curr == 0 (batch is empty), the group is happy. Recurse and take the maximum.
  7. Memoization: Since the number of possible states is small (because batchSize is at most 9 and there are at most 30 groups), we can cache results for each unique state to avoid recomputation.

This approach is efficient and leverages the small size of batchSize and the fact that only the counts of each remainder matter, not the specific group sizes.

Example Walkthrough

Suppose batchSize = 3 and groups = [1,2,3,4,5,6].

  • Compute remainders: [1,2,0,1,2,0]
  • Happy groups from remainder 0: There are two such groups (3 and 6). Add 2 to the happy count.
  • Remainder counts: [0,2,2] (for remainders 0,1,2).
  • Now, consider how to pair remainder 1 and remainder 2 groups. If we take a group with remainder 1, then remainder 2 (or vice versa), the sum is 3, which resets the batch, making the next group happy.
  • Through recursive DP, we find the optimal order. For this example, the best we can do is serve (1,2), (4,5), and the two remainder-0 groups, for a total of 4 happy groups.

The DP ensures we consider all possible valid orders and pick the one that maximizes happy groups.

Time and Space Complexity

Brute-force Approach: Trying all permutations is O(n!), which is infeasible for n up to 30.

Optimized (DP with Memoization):

  • The state space is determined by the number of groups for each remainder. Since batchSize is at most 9 and the total number of groups is at most 30, the number of possible states is manageable.
  • For each DP state, we try up to batchSize options, and memoize results. The total number of states is bounded by (number of ways to distribute 30 indistinguishable balls into (batchSize-1) bins), which is combinatorial but tractable for small batchSize.
  • Time Complexity: O((groups.length)^(batchSize-1)), but with small constants and memoization, it's fast enough.
  • Space Complexity: O((groups.length)^(batchSize-1)) for the memoization table.

Summary

The core insight is reducing the problem to remainders modulo batchSize and using dynamic programming with memoization to efficiently explore all possible groupings. By focusing on remainder counts rather than actual group sizes, and leveraging the small batchSize, we avoid brute-force and find the optimal solution efficiently. The approach is both elegant and practical, making use of combinatorial state compression and recursive DP.

Code Implementation

from functools import lru_cache
from collections import Counter

class Solution:
    def maxHappyGroups(self, batchSize: int, groups: list[int]) -> int:
        rems = [g % batchSize for g in groups]
        cnt = [0] * batchSize
        for r in rems:
            cnt[r] += 1
        res = cnt[0]

        @lru_cache(maxsize=None)
        def dfs(state, curr):
            ans = 0
            for i in range(1, batchSize):
                if state[i]:
                    new_state = list(state)
                    new_state[i] -= 1
                    # If curr == 0, this group is happy
                    add = 1 if curr == 0 else 0
                    nxt = (curr + i) % batchSize
                    ans = max(ans, dfs(tuple(new_state), nxt) + add)
            return ans

        # Start DFS, state is the tuple of counts (cnt[1], ..., cnt[batchSize-1])
        return res + dfs(tuple(cnt), 0)
      
#include <vector>
#include <unordered_map>
#include <algorithm>

using namespace std;

class Solution {
public:
    int batchSize;
    unordered_map<string, int> memo;
    
    int dfs(vector<int>& state, int curr) {
        string key;
        for (int i = 1; i < batchSize; ++i) {
            key += to_string(state[i]) + ",";
        }
        key += to_string(curr);
        if (memo.count(key)) return memo[key];
        int ans = 0;
        for (int i = 1; i < batchSize; ++i) {
            if (state[i]) {
                state[i]--;
                int add = (curr == 0) ? 1 : 0;
                int nxt = (curr + i) % batchSize;
                ans = max(ans, dfs(state, nxt) + add);
                state[i]++;
            }
        }
        memo[key] = ans;
        return ans;
    }
    
    int maxHappyGroups(int batchSize_, vector<int>& groups) {
        batchSize = batchSize_;
        vector<int> cnt(batchSize, 0);
        for (int g : groups) cnt[g % batchSize]++;
        int res = cnt[0];
        return res + dfs(cnt, 0);
    }
};
      
import java.util.*;

class Solution {
    int batchSize;
    Map<String, Integer> memo = new HashMap<>();

    public int maxHappyGroups(int batchSize, int[] groups) {
        this.batchSize = batchSize;
        int[] cnt = new int[batchSize];
        for (int g : groups) cnt[g % batchSize]++;
        int res = cnt[0];
        return res + dfs(cnt, 0);
    }

    private int dfs(int[] state, int curr) {
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i < batchSize; ++i) {
            sb.append(state[i]).append(",");
        }
        sb.append(curr);
        String key = sb.toString();
        if (memo.containsKey(key)) return memo.get(key);
        int ans = 0;
        for (int i = 1; i < batchSize; ++i) {
            if (state[i] > 0) {
                state[i]--;
                int add = (curr == 0) ? 1 : 0;
                int nxt = (curr + i) % batchSize;
                ans = Math.max(ans, dfs(state, nxt) + add);
                state[i]++;
            }
        }
        memo.put(key, ans);
        return ans;
    }
}
      
var maxHappyGroups = function(batchSize, groups) {
    let cnt = Array(batchSize).fill(0);
    for (let g of groups) cnt[g % batchSize]++;
    let res = cnt[0];
    let memo = new Map();

    function dfs(state, curr) {
        let key = state.slice(1).join(',') + ',' + curr;
        if (memo.has(key)) return memo.get(key);
        let ans = 0;
        for (let i = 1; i < batchSize; ++i) {
            if (state[i]) {
                state[i]--;
                let add = (curr === 0) ? 1 : 0;
                let nxt = (curr + i) % batchSize;
                ans = Math.max(ans, dfs(state, nxt) + add);
                state[i]++;
            }
        }
        memo.set(key, ans);
        return ans;
    }

    return res + dfs(cnt, 0);
};