#!/usr/bin/env python # coding: utf-8 # # More memoization: getting the optimal solution and not just its value # ### Figuring out which items to actually put in the box to get the most value (from yesterday) # # As asked yesterday: we saw how to figure out how to optimize a function using recursion/memoization, but what if we want to remember the decisions we made to obtain the optimal value? # # As discussed yesterday, suppose you have a box that can hold $W$ kilos. You are given a list $L$ of items $[[w_0,v_0],\ldots, [w_{n-1},v_{n-1}]]$ where the $i^{th}$ item weighs $w_i$ kilos and is worth $v_i$ birr. Yesterday we asked: is the maximum amount of value you can fit into the box? # # Today we will ask the question: what if you want to know specifically *which* items to pack in the box to have the most value without breaking the box? # In[2]: def mostValueMemo(W, L, mem, choices): """returns the maximum amount of value we can pack in <= W kilos, using the items listed in L, where L is a tuple/list containing pairs (w,v) of weight/value for every item.""" # base case if len(L)==0: # if L is empty then we have no items to pack return 0 # check memory elif mem[W][len(L)] != -1: return mem[W][len(L)] # recursive case # max value we can get if we don't pack the first item A = mostValueMemo(W, L[1:], mem, choices) # if we can't fit the first item then we have no choice to make if W B: mem[W][len(L)] = A choices[W][len(L)] = 'D' else: mem[W][len(L)] = B choices[W][len(L)] = 'P' # 'P' means "Pack" the first item return mem[W][len(L)] def initMem(n, m, initValue): # returns a 2-dimensional list of lists mem, where mem has n lists each of size m mem = [] for i in range(n): mem += [[-1]*m] return mem def mostValueFast(W, L, choices): mem = initMem(W+1, len(L)+1, -1) return mostValueMemo(W, L, mem, choices) def whichItems(W, L): choices = initMem(W+1, len(L)+1, -1) mostValueFast(W, L, choices) items = [] while len(L) > 0: c = choices[W][len(L)] if c == 'P': items += [L[0]] W -= L[0][0] L = L[1:] return items # In[20]: whichItems(4,((1,6),(2,5),(3,6),(2,10))) # In[3]: whichItems(4, [[3,9], [2,5], [2,6]]) # ### Another example: making change # # We saw an example problem yesterday of calculating the number of ways to make change for n cents using a list of coins L. While this is more of a toy problem, a more real problem comes from the fact that many people do not like to carry lots of coins around. Thus when a shopkeeper makes change for $n$ cents, as a service to the customer it mayu be desired to make change for n cents using as few coins as possible (in fact, in Japan sometimes change is even rounded up to minimize the number of coins returned, with "サービス", or "service", written on the receipt!). For example, in the Ethiopian coin system one could make change for $n=8$ cents by giving eight 1-cent pieces, for a total of 8 coins. A better way to make change though is to give a five 1-cent pieces and one 5-cent piece, for a total of only 6 coins. We would like to write a function that tells the shopkeeper which coins to give to the customer to make change for $n$ cents so as to minimize the number of coins. # # First, let us write a recursive function ```fewestCoins``` which just tells us what the minimum number of coins to use is. We will use the convention that if it is impossible to make change for $n$ cents, then the number of coins required is "infinity". Python has a built-in infinity value, ```float('infinity')```, which we will use to denote this (it behaves as one would expect, e.g. infinity plus or minus any other finite value is infinity, etc.). # In[4]: def fewestCoins(n, L): # base case if len(L) == 0: if n == 0: return 0 else: return float('infinity') # recursive case else: # we can either not use coin L[0] at all, option A, or use it at least once, option B A = fewestCoins(n, L[1:]) if L[0] > n: # coin L[0] is too big, so we can't use it return A B = 1 + fewestCoins(n - L[0], L) return min(A, B) # In[5]: print(fewestCoins(14, [1,5,10,25,50,100])) # should print 5 print(fewestCoins(3, [2,5])) # should print inf (short for "infinity") print(fewestCoins(8, [1,4,5])) # should print 2 since it is better to use two 4-cent pieces than one 5-cent pieces and three 1-cent pieces # Now let's throw some memoization on this $\ldots$ # In[6]: def fewestCoinsMemo(n, L, mem, seen): # base case if len(L) == 0: if n == 0: return 0 else: return float('infinity') # check if we've already computed this answer if seen[n][len(L)]: return mem[n][len(L)] # recursive case else: seen[n][len(L)] = True # we can either not use coin L[0] at all, option A, or use it at least once, option B A = fewestCoinsMemo(n, L[1:], mem, seen) if L[0] > n: # coin L[0] is too big, so we can't use it mem[n][len(L)] = A return A B = 1 + fewestCoinsMemo(n - L[0], L, mem, seen) mem[n][len(L)] = min(A, B) return min(A, B) def fewestCoinsFast(n, L): # mem[i][j] stores the minimum number of coins needed to make change for i cents # when using the coins L[len(L)-j:] mem = [] seen = [] for i in range(n+1): mem += [[-1]*(len(L)+1)] seen += [[False]*(len(L)+1)] return fewestCoinsMemo(n, L, mem, seen) # In[8]: print(fewestCoinsFast(14, [1,5,10,25,50,100])) # should print 5 print(fewestCoinsFast(3, [2,5])) # should print inf (short for "infinity") print(fewestCoinsFast(8, [1,4,5])) # should print 2 since it is better to use two 4-cent pieces than one 5-cent pieces and three 1-cent pieces # Now back to the point of today's lecture: how can we trace the ```mem``` table to know which coins to actually give back to the customer? We will return the list of coins if possible to make change, and otherwise we will return ```float('infinity')```. # In[14]: def fewestCoinsMemo(n, L, mem, seen, choices): # base case if len(L) == 0: if n == 0: return 0 else: return float('infinity') # check if we've already computed this answer if seen[n][len(L)]: return mem[n][len(L)] # recursive case else: seen[n][len(L)] = True choices[n][len(L)] = False # we can either not use coin L[0] at all, option A, or use it at least once, option B A = fewestCoinsMemo(n, L[1:], mem, seen, choices) if L[0] > n: # coin L[0] is too big, so we can't use it mem[n][len(L)] = A return A B = 1 + fewestCoinsMemo(n - L[0], L, mem, seen, choices) mem[n][len(L)] = min(A, B) if B 0: c = choices[n][len(L)] if c: coins += [L[0]] n -= L[0] else: L = L[1:] return coins # In[15]: print(whichCoins(14, [1,5,10,25,50,100])) # should print 5 print(whichCoins(3, [2,5])) # should print inf (short for "infinity") print(whichCoins(8, [1,4,5])) # should print 2 since it is better to use two 4-cent pieces than one 5-cent pieces and three 1-cent pieces