2305. Fair Distribution of Cookies
Problem Description
You have an integer array cookies
where cookies[i]
represents the number of cookies in the i
-th bag. You need to distribute all these bags to k
children with the following constraints:
- Each bag must be given to exactly one child (you cannot split a bag between multiple children)
- All cookies in a bag stay together
The unfairness of a distribution is defined as the maximum total number of cookies that any single child receives.
Your task is to find the minimum possible unfairness across all valid distributions.
For example, if you have cookies = [8, 15, 10, 20, 8]
and k = 2
children:
- One possible distribution: Child 1 gets bags with [8, 15, 8] (total = 31 cookies), Child 2 gets bags with [10, 20] (total = 30 cookies). The unfairness is max(31, 30) = 31.
- Another distribution: Child 1 gets bags with [20, 8] (total = 28 cookies), Child 2 gets bags with [8, 15, 10] (total = 33 cookies). The unfairness is max(28, 33) = 33.
The goal is to find the distribution strategy that minimizes this maximum value (the unfairness).
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem doesn't involve nodes and edges or graph traversal. We're dealing with distributing bags of cookies to children.
Need to solve for kth smallest/largest?
- No: We're not looking for a kth smallest or largest element. We need to find the minimum possible unfairness value.
Involves Linked Lists?
- No: The problem uses an array of cookies, not linked lists.
Does the problem have small constraints?
- Yes: Looking at the problem, we need to distribute bags to
k
children, and typically these distribution problems have relatively small constraints (usuallyk ≤ 10
and array length ≤ 15-20). We need to explore all possible ways to distribute the bags.
Brute force / Backtracking?
- Yes: Since we have small constraints and need to explore all possible distributions to find the optimal one, backtracking is the appropriate approach. We need to:
- Try assigning each bag to each possible child
- Track the current distribution's unfairness
- Backtrack when we've assigned all bags or when a distribution exceeds our current best
- Explore all possibilities to find the minimum unfairness
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to exhaustively search through all possible ways to distribute the cookie bags among the children, keeping track of the minimum unfairness found, and pruning branches that cannot lead to better solutions.
Intuition
When we need to distribute items among multiple recipients and find the optimal distribution, we're essentially dealing with a decision tree where at each step we decide which recipient gets which item. Since we want to minimize the maximum cookies any child receives, we need to explore different distribution strategies.
The key insight is that this is a classic assignment problem with a twist - we care about minimizing the maximum load rather than the total. Think of it like distributing work among employees where you want to ensure no one is overworked. We can't use a greedy approach (like always giving to the child with the least cookies) because local optimal choices don't guarantee a global optimum.
For example, if we have cookies [20, 15, 10]
and 2 children, greedily giving the largest bag first might not lead to the best solution. We need to try different combinations to find the truly optimal distribution.
Since we must try all possible distributions to guarantee finding the minimum unfairness, backtracking becomes our natural choice. We systematically:
- Take a bag of cookies
- Try giving it to each child
- Recursively distribute the remaining bags
- Track the maximum cookies any child gets in each complete distribution
- Backtrack to try other possibilities
To make this efficient, we can add pruning strategies:
- Sort cookies in descending order to fail fast (larger values are more likely to exceed our current best answer early)
- Skip distributions where a child already has more cookies than our current best answer
- Skip giving cookies to a child if the previous child has the same amount (avoids duplicate distributions)
This exhaustive search with intelligent pruning ensures we find the minimum possible unfairness while avoiding unnecessary computation.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to explore all possible distributions of cookie bags to children.
Data Structures:
cnt
: An array of sizek
that tracks the current number of cookies each child hasans
: A variable to store the minimum unfairness found so far, initialized to infinity
Algorithm Steps:
-
Preprocessing: Sort the
cookies
array in descending order. This optimization helps with pruning - when we assign larger cookie bags first, we can more quickly identify and skip branches that will exceed our current best answer. -
DFS Function: The recursive
dfs(i)
function handles distributing thei
-th bag of cookies:-
Base Case: When
i >= len(cookies)
, all bags have been distributed. Calculate the current unfairness asmax(cnt)
and updateans
if this distribution is better. -
Recursive Case: For the current bag
i
, try giving it to each childj
:Pruning conditions (skip if):
cnt[j] + cookies[i] >= ans
: Adding this bag to childj
would already meet or exceed our current best unfairness, so no point exploring this branchj > 0 and cnt[j] == cnt[j-1]
: If the current child has the same number of cookies as the previous child, giving the bag to either produces equivalent distributions (avoids duplicates)
Backtracking steps:
- Add
cookies[i]
to childj
's count:cnt[j] += cookies[i]
- Recursively distribute remaining bags:
dfs(i + 1)
- Backtrack by removing the cookies:
cnt[j] -= cookies[i]
-
-
Main Execution:
- Initialize
cnt
with zeros andans
with infinity - Start the DFS from bag 0
- Return the minimum unfairness found
- Initialize
The sorting optimization combined with the two pruning conditions significantly reduces the search space. The first pruning condition acts as an upper bound check, while the second eliminates symmetric distributions, making the algorithm efficient enough for the given constraints.
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 trace through a small example with cookies = [6, 4, 5]
and k = 2
children.
Step 1: Preprocessing
- Sort cookies in descending order:
[6, 5, 4]
- Initialize
cnt = [0, 0]
(tracking cookies for each child) - Initialize
ans = infinity
Step 2: DFS Exploration
Starting with dfs(0)
- distributing bag with 6 cookies:
Branch 1: Give bag 0 (6 cookies) to child 0
-
cnt = [6, 0]
-
Call
dfs(1)
- distributing bag with 5 cookiesBranch 1.1: Give bag 1 (5 cookies) to child 0
cnt = [11, 0]
- Call
dfs(2)
- distributing bag with 4 cookies- Must give to child 1 (child 0 already has 11)
cnt = [11, 4]
- All bags distributed: unfairness = max(11, 4) = 11
- Update
ans = 11
- Backtrack:
cnt = [11, 0]
- Backtrack:
cnt = [6, 0]
Branch 1.2: Give bag 1 (5 cookies) to child 1
-
cnt = [6, 5]
-
Call
dfs(2)
- distributing bag with 4 cookiesBranch 1.2.1: Give to child 0
cnt = [10, 5]
- All bags distributed: unfairness = max(10, 5) = 10
- Update
ans = 10
(better than 11) - Backtrack:
cnt = [6, 5]
Branch 1.2.2: Give to child 1
cnt = [6, 9]
- All bags distributed: unfairness = max(6, 9) = 9
- Update
ans = 9
(better than 10) - Backtrack:
cnt = [6, 5]
-
Backtrack:
cnt = [6, 0]
-
Backtrack:
cnt = [0, 0]
Branch 2: Give bag 0 (6 cookies) to child 1
-
cnt = [0, 6]
-
Call
dfs(1)
- distributing bag with 5 cookiesBranch 2.1: Give bag 1 (5 cookies) to child 0
-
cnt = [5, 6]
-
Call
dfs(2)
- distributing bag with 4 cookiesBranch 2.1.1: Give to child 0
cnt = [9, 6]
- Pruning: 9 >= ans (9), skip this branch
- Backtrack:
cnt = [5, 6]
Branch 2.1.2: Give to child 1
cnt = [5, 10]
- Pruning: 10 >= ans (9), skip this branch
- Backtrack:
cnt = [5, 6]
-
Backtrack:
cnt = [0, 6]
Branch 2.2: Give bag 1 (5 cookies) to child 1
cnt = [0, 11]
- Pruning: 11 >= ans (9), skip entire subtree
- Backtrack:
cnt = [0, 6]
-
-
Backtrack:
cnt = [0, 0]
Result: The minimum unfairness is 9, achieved when:
- Child 0 gets bag with 6 cookies
- Child 1 gets bags with 5 and 4 cookies (total = 9)
The pruning conditions helped us skip several branches:
- When a child's current total would exceed our best answer
- This reduced our search from exploring all 8 possible distributions to examining only the necessary ones
Solution Implementation
1class Solution:
2 def distributeCookies(self, cookies: List[int], k: int) -> int:
3 # Initialize the minimum unfairness to infinity
4 self.min_unfairness = float('inf')
5
6 # Array to track the total cookies each child has
7 children_cookies = [0] * k
8
9 # Sort cookies in descending order for better pruning
10 cookies.sort(reverse=True)
11
12 def backtrack(cookie_index: int) -> None:
13 """
14 Recursively distribute cookies using backtracking.
15
16 Args:
17 cookie_index: Current index of cookie being distributed
18 """
19 # Base case: all cookies have been distributed
20 if cookie_index >= len(cookies):
21 # Calculate unfairness (maximum cookies any child has)
22 current_unfairness = max(children_cookies)
23 self.min_unfairness = min(self.min_unfairness, current_unfairness)
24 return
25
26 # Try giving current cookie to each child
27 for child in range(k):
28 # Pruning condition 1: Skip if giving this cookie would exceed current best
29 if children_cookies[child] + cookies[cookie_index] >= self.min_unfairness:
30 continue
31
32 # Pruning condition 2: Skip duplicate states (consecutive children with same amount)
33 if child > 0 and children_cookies[child] == children_cookies[child - 1]:
34 continue
35
36 # Give cookie to current child
37 children_cookies[child] += cookies[cookie_index]
38
39 # Recursively distribute remaining cookies
40 backtrack(cookie_index + 1)
41
42 # Backtrack: take cookie back from current child
43 children_cookies[child] -= cookies[cookie_index]
44
45 # Start the backtracking process
46 backtrack(0)
47
48 return self.min_unfairness
49
1class Solution {
2 private int[] cookies; // Array of cookie bags to distribute
3 private int[] childrenLoad; // Current cookie count for each child
4 private int k; // Number of children
5 private int n; // Number of cookie bags
6 private int minUnfairness = 1 << 30; // Minimum unfairness found (initialized to large value)
7
8 public int distributeCookies(int[] cookies, int k) {
9 this.n = cookies.length;
10 this.childrenLoad = new int[k];
11
12 // Sort cookies in ascending order for optimization
13 Arrays.sort(cookies);
14 this.cookies = cookies;
15 this.k = k;
16
17 // Start DFS from the largest cookie (index n-1) to smallest (index 0)
18 // This helps with pruning as larger values have more impact
19 dfs(n - 1);
20
21 return minUnfairness;
22 }
23
24 private void dfs(int cookieIndex) {
25 // Base case: all cookies have been distributed
26 if (cookieIndex < 0) {
27 // Calculate the maximum cookies any child has (unfairness)
28 int maxLoad = 0;
29 for (int load : childrenLoad) {
30 maxLoad = Math.max(maxLoad, load);
31 }
32 minUnfairness = Math.min(minUnfairness, maxLoad);
33 return;
34 }
35
36 // Try giving current cookie to each child
37 for (int childIndex = 0; childIndex < k; childIndex++) {
38 // Pruning condition 1: Skip if giving this cookie would exceed current best unfairness
39 if (childrenLoad[childIndex] + cookies[cookieIndex] >= minUnfairness) {
40 continue;
41 }
42
43 // Pruning condition 2: Skip if current child has same load as previous child
44 // (avoids redundant permutations since children are indistinguishable)
45 if (childIndex > 0 && childrenLoad[childIndex] == childrenLoad[childIndex - 1]) {
46 continue;
47 }
48
49 // Give cookie to current child
50 childrenLoad[childIndex] += cookies[cookieIndex];
51
52 // Recursively distribute remaining cookies
53 dfs(cookieIndex - 1);
54
55 // Backtrack: remove cookie from current child
56 childrenLoad[childIndex] -= cookies[cookieIndex];
57 }
58 }
59}
60
1class Solution {
2public:
3 int distributeCookies(vector<int>& cookies, int k) {
4 // Sort cookies in descending order for better pruning
5 sort(cookies.rbegin(), cookies.rend());
6
7 // Array to track the total cookies each child receives
8 int childrenCookies[k];
9 memset(childrenCookies, 0, sizeof(childrenCookies));
10
11 int numCookies = cookies.size();
12 int minUnfairness = INT_MAX;
13
14 // DFS function to try all possible distributions
15 function<void(int)> dfs = [&](int cookieIndex) {
16 // Base case: all cookies have been distributed
17 if (cookieIndex >= numCookies) {
18 // Find the maximum cookies any child has (unfairness)
19 int currentUnfairness = *max_element(childrenCookies, childrenCookies + k);
20 minUnfairness = min(minUnfairness, currentUnfairness);
21 return;
22 }
23
24 // Try giving current cookie to each child
25 for (int childIndex = 0; childIndex < k; ++childIndex) {
26 // Pruning condition 1: Skip if current distribution already exceeds minimum unfairness
27 if (childrenCookies[childIndex] + cookies[cookieIndex] >= minUnfairness) {
28 continue;
29 }
30
31 // Pruning condition 2: Skip duplicate states (if previous child has same cookies)
32 if (childIndex > 0 && childrenCookies[childIndex] == childrenCookies[childIndex - 1]) {
33 continue;
34 }
35
36 // Give cookie to current child
37 childrenCookies[childIndex] += cookies[cookieIndex];
38
39 // Recursively distribute remaining cookies
40 dfs(cookieIndex + 1);
41
42 // Backtrack: take cookie back from current child
43 childrenCookies[childIndex] -= cookies[cookieIndex];
44 }
45 };
46
47 // Start the DFS from the first cookie
48 dfs(0);
49
50 return minUnfairness;
51 }
52};
53
1/**
2 * Distributes cookies among k children to minimize the maximum total cookies any child receives
3 * @param cookies - Array of cookie bags where cookies[i] represents the number of cookies in the i-th bag
4 * @param k - Number of children to distribute cookies to
5 * @returns The minimum possible maximum total cookies any child can receive
6 */
7function distributeCookies(cookies: number[], k: number): number {
8 // Array to track the current distribution of cookies to each child
9 const childCookieCounts: number[] = new Array(k).fill(0);
10
11 // Initialize answer with a large value (2^30)
12 let minMaxCookies: number = 1 << 30;
13
14 /**
15 * Depth-first search to try all possible distributions
16 * @param cookieIndex - Current index of the cookie bag being distributed
17 */
18 const distributeRecursively = (cookieIndex: number): void => {
19 // Base case: all cookies have been distributed
20 if (cookieIndex >= cookies.length) {
21 // Update the answer with the maximum cookies any child has
22 minMaxCookies = Math.min(minMaxCookies, Math.max(...childCookieCounts));
23 return;
24 }
25
26 // Try giving the current cookie bag to each child
27 for (let childIndex = 0; childIndex < k; childIndex++) {
28 // Pruning condition 1: Skip if giving this bag would exceed current best answer
29 // Pruning condition 2: Skip duplicate states (when previous child has same count)
30 if (childCookieCounts[childIndex] + cookies[cookieIndex] >= minMaxCookies ||
31 (childIndex > 0 && childCookieCounts[childIndex] === childCookieCounts[childIndex - 1])) {
32 continue;
33 }
34
35 // Give current cookie bag to this child
36 childCookieCounts[childIndex] += cookies[cookieIndex];
37
38 // Recursively distribute remaining cookies
39 distributeRecursively(cookieIndex + 1);
40
41 // Backtrack: remove the cookie bag from this child
42 childCookieCounts[childIndex] -= cookies[cookieIndex];
43 }
44 };
45
46 // Start the distribution process from the first cookie bag
47 distributeRecursively(0);
48
49 return minMaxCookies;
50}
51
Time and Space Complexity
Time Complexity: O(k^n)
where n
is the number of cookies and k
is the number of children.
The algorithm uses backtracking to try all possible distributions of cookies among k children. For each cookie (n total cookies), we have at most k choices of which child to give it to. This creates a recursion tree with:
- Depth:
n
(one level for each cookie) - Branching factor: at most
k
at each level
Although there are pruning conditions (cnt[j] + cookies[i] >= ans
and cnt[j] == cnt[j - 1]
) that reduce the actual number of branches explored, the worst-case scenario still requires exploring k^n
possibilities.
Space Complexity: O(n + k)
The space complexity consists of:
- Recursion call stack:
O(n)
- The maximum depth of recursion isn
(the number of cookies) - Array
cnt
:O(k)
- Stores the current distribution of cookies for k children - Sorting the cookies array is done in-place, so no additional space for that
- Variable
ans
:O(1)
Therefore, the total space complexity is O(n + k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Missing Early Termination for Empty Children
The Pitfall:
The current pruning condition if child > 0 and children_cookies[child] == children_cookies[child - 1]
only avoids duplicate states between consecutive children with the same cookie count. However, when multiple children have 0 cookies (which is common early in the recursion), the algorithm still explores giving the current bag to each of these empty children separately, even though these distributions are equivalent.
For example, with 3 children all having 0 cookies, the algorithm tries:
- Give bag to child 0 (explores full subtree)
- Give bag to child 1 (explores same subtree again)
- Give bag to child 2 (explores same subtree yet again)
The Solution: Add an early termination when encountering an empty child. Once we've explored giving a bag to one empty child, we don't need to try other empty children:
for child in range(k):
# Pruning condition 1: Skip if exceeds current best
if children_cookies[child] + cookies[cookie_index] >= self.min_unfairness:
continue
# Pruning condition 2: Skip duplicate states
if child > 0 and children_cookies[child] == children_cookies[child - 1]:
continue
# Give cookie to current child
children_cookies[child] += cookies[cookie_index]
# Recursively distribute remaining cookies
backtrack(cookie_index + 1)
# Backtrack
children_cookies[child] -= cookies[cookie_index]
# IMPORTANT: If this child had 0 cookies, no need to try other empty children
if children_cookies[child] == 0:
break
2. Integer Overflow with Large Cookie Values
The Pitfall: While Python handles arbitrary precision integers automatically, in other languages or when optimizing with NumPy arrays, the sum of cookies could overflow standard integer types if cookie values are large.
The Solution: Ensure appropriate data types are used for tracking cookie counts, or add validation for input constraints:
# Add input validation if needed
if any(c > 10**9 for c in cookies) or len(cookies) * max(cookies) > 10**18:
# Handle potential overflow scenarios
pass
3. Not Handling Edge Case: k > len(cookies)
The Pitfall: When there are more children than cookie bags, some children will inevitably get 0 cookies. The current solution handles this correctly, but it's worth noting that the minimum unfairness in this case is always 0 (since at least one child gets nothing, and we can give all bags to one child).
The Solution: Add an optimization for this edge case:
def distributeCookies(self, cookies: List[int], k: int) -> int:
# Edge case: more children than bags
if k > len(cookies):
return max(cookies) # Best we can do is give one bag to one child
# Rest of the implementation...
Which of the following uses divide and conquer strategy?
Recommended Readings
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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!