See all coding puzzles and patterns here.
  • Difficulty: Medium
  • Patterns:

Problem Definition

You are given an array of magic bags containing different numbers of candies. It takes you 1 minute to eat a full bag of candies regardless of the number of candies in the bag. Once you finish eating the candies, the bag is magically refilled so that if the bag initially contained x candies then at the end of the minute, the bag is refilled to contain floor(x / 2) candies. Determine how many candies you can eat in $k$ minutes.


Examples

Some test cases to verify our solution.

assert candies([2, 1, 7, 4, 2], 3) == 14 # because => 7 [2, 1, 3, 4, 2] => 7+4 [2, 1, 3, 2, 2] =>
    # 7+4+3 [2, 1, 1, 2, 2]
assert candies([19, 78, 76, 72, 48, 8, 24, 74, 29], 3) == 228

Brute-Force Solution

We will sort the array of candy bags and then eat from the bag with the most candies. After that, we will put the bag back into the array of bags and sort the whole array.

def candies(arr, k):
    l = sorted(arr)
    candies = 0
    for _ in range(k):
        bag = l.pop()
        candies += bag
        l.append(bag // 2)
        l = sorted(l)
    return candies

Brute-Force Solution Complexity

Let $n = $ len(arr).

Runtime complexity should be $O(k \cdot n \cdot log(n))$. Our for loop has $k$ iterations, and in each iteration, we are sorting an array of length $n$, which results in a nested complexity of $O(n \cdot log(n))$. Auxiliary space complexity is $O(n)$ because we are creating a copy of arr.


Improved Solution

We can remove the $log(n)$ factor by traversing the array at every iteration to find the largest element.

def candies(arr, k):
    candies = 0
    for _ in range(k):
        bag = max(arr)
        arr.remove(bag)
        candies += bag
        arr.append(bag // 2)
    return candies

Improved Solution Complexity

Let $n = $ len(arr).

Runtime complexity should be $O(k \cdot n)$. Our for loop has $k$ iterations, and in each iteration, we are traversing an array of length $n$, which results in a nested complexity of $O(n)$.

Auxiliary space complexity is $O(1)$ but we are destroying arr.


Heap Solution

We can use a max-heap to simplify our code and improve runtime complexity.

import heapq

def candies(arr, k):
    heapq._heapify_max(arr) # nlogn
    candies = 0
    for _ in range(k): # k
        bag = arr[0]
        candies += bag
        heapq._heapreplace_max(arr, bag // 2) # logn
    return candies

Heap Solution Complexity

Let $n = $ len(arr).

Runtime complexity should be $O(n \cdot log(n) + k \cdot log(n)) = O((n + k) \cdot log(n))$. The construction of the heap has a complexity of $O(n \cdot log(n))$. Our for loop has $k$ iterations, and in each iteration, we are doing a pop_push into a heap of size $n$, resulting in a nested complexity of $O(log(n))$.

Auxiliary space complexity is $O(1)$ because we are modifying arr in place.