The proposal below does a brute force search, using as possible moves the size of each cookie pile (I'm not sure this ensures that this ensures optimality, as in principle a number not in the pile could be optimal). Also, since a brute force is done, in the worst case a factorial number of cases will be tried, so for inputs larger than 10 or 11 the execution time (and memory) may become unmanageable (but I'll show how to improve on that later).
The recursion depth is the length of the input - 2, so quite shallow. The algorithm is pretty simple: at any step, for all possible moves, it selects a move and then solve the sub-problem after that move is made. Since every movie eats at least one pile, the number of piles is always smaller at every step. It selects the shortest solution out of all possible ones. We could have done a slight optimisation here in that, since we know the smallest possible size of a solution, if we found a solution with this size we stop searching for more, since we cannot do better than the current one anyway.
An additional optimisation that was not done is that the question requests the length of the smallest solution, and the code below produces a sequence of moves that is (hopefully) minimal.
def solve_cookie_monster_problem(piles):
if len(piles) <= 2: # This is the end of recursion condition. 2 jars always need two moves
return list(piles)
# Try each element in piles as a move, and return the solution with the least moves
best_try = range(len(piles) + 1) # This is longer than any try
for move in piles:
next_piles = {(el - move) if el > move else el
for el in piles if el != move}
current_try = solve_cookie_monster_problem(next_piles)
current_try.append(move)
if len(current_try) < len(best_try):
best_try = current_try
# best_try contains the shortest solution
return best_try
The result is quite slow on my machine (all tests cases below run in about 5 s). There is a simple way to improve significantly this algorithm, if we note that each recursion step might visit problems that were already solved before: we can memoise the function, i.e. record previous results and simply return then, if we see a call with the same configuration of cookies. This is extremely simple to implement with python dictionaries. This simple improvement reduced the execution time from 5s to about 0.25s in the test cases below.
def solve_cookie_monster_memoised(initial_piles):
"""Memoised version of solve_cookie_monster.
Essentially the same algorithm, but checking if that particular sub-problem hasn't been solved before."""
memory = {} # Will record subproblems already solved
def get(value): return list(memory[value]) # Always create a copy when accessing the memoised
initial_piles = frozenset(initial_piles)
def algorithm(piles):
if piles in memory:
return get(piles)
if len(piles) <= 2: # This is the end of recursion condition. 2 jars always need two moves
memory[piles] = list(piles)
return get(piles)
# Try each element in piles as a move, and return the solution with the least moves
best_try = range(len(piles) + 1) # This is longer than any try
for move in piles:
next_piles = frozenset({(el - move) if el > move else el
for el in piles if el != move})
current_try = algorithm(next_piles)
current_try.append(move)
if len(current_try) < len(best_try):
best_try = current_try
memory[piles] = best_try
return get(piles)
return algorithm(initial_piles)
Here are some test cases:
if __name__ == '__main__':
import time
piles1 = [2, 3, 5, 8, 13, 21, 34, 55, 89]
piles2 = [11, 26, 37, 44, 49, 52, 68, 75, 87, 102]
piles3 = [7, 4, 3, 1]
piles4 = [20, 19, 14, 7, 4]
piles5 = [18, 16, 9, 8]
piles6 = [11, 26, 37, 44, 49, 52, 68, 75, 87, 89, 102]
tstart = time.perf_counter()
print(solve_cookie_monster_problem(piles1)) # [2, 3, 8, 21, 55]
print(solve_cookie_monster_problem(piles2)) # [3, 4, 7, 15, 34, 68]
print(solve_cookie_monster_problem(piles3)) # [1, 4, 3]
print(solve_cookie_monster_problem(piles4)) # [1, 2, 4, 14]
print(solve_cookie_monster_problem(piles5)) # [1, 2, 8, 16]
print(solve_cookie_monster_problem(piles6)) # [1, 3, 7, 16, 37, 49]
tend = time.perf_counter()
print(f'Time: {tend-tstart:.2f}') # About 5s on my machine
print('\nMemoised version')
tstart = time.perf_counter()
print(solve_cookie_monster_memoised(piles1)) # [2, 3, 8, 21, 55]
print(solve_cookie_monster_memoised(piles2)) # [3, 4, 7, 15, 34, 68]
print(solve_cookie_monster_memoised(piles3)) # [1, 4, 3]
print(solve_cookie_monster_memoised(piles4)) # [1, 2, 4, 14]
print(solve_cookie_monster_memoised(piles5)) # [1, 2, 8, 16]
print(solve_cookie_monster_memoised(piles6)) # [1, 3, 7, 16, 37, 49]
tend = time.perf_counter()
print(f'Time: {tend-tstart:.2f}') # About 0.25s on my machine
A modification of this code to return only the length is trivial, and will improve a bit the performance (about 10% in the non-memoised version).