#!/usr/bin/env python # coding: utf-8 # # Memoization # # Memoization is a general technique for speeding up recursive algorithms. Specifically, in this technique if there is some function ```f``` taking in some inputs ```p```, we maintain a lookup table on the side indexed by possible such inputs. If ```f(p)``` has already been calculated in the past, we return ```f(p)``` from the lookup table. Otherwise, we compute it from scratch, insert it into the lookup table, then return it. We illustrate this technique below using an example. # In[ ]: # We will show how we can use memoization to make significant savings in running time. # # # Example 1: Packing items into boxes # # You have a box that can hold $W$ kilos (if you try to put more, it breaks). 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 if you sell it in the market. What is the maximum amount of value you can fit in your box without the box breaking? There is only one of each item (you can't put multiple copies of the same item in your box). # We start by solving this with a simple recursive procedure. # One way to think about this question is as follows: # # * If there was only one item that has weight $w$ and has value $v$, then our decision is simple: if $W \geq w$ then we can fit the item and get $v$ value, and otherwise we get zero value (no items). # # * If there are two items with weight/value $(w_0,v_0)$ and $(w_1,v_1)$ then we might have a choice to make. It's often good in such cases to split a complicated choice to a sequence of simple choices. So let's start with the choice of whether or not to put the $0$th item in the box. If we do take it, then we'll get $v_0$ value, and be left with a box that can only fit $W-w_0$ kilos more. If we don't then we get zero value, and are left with $W$ kilos to pack. # # * Therefore if $\mathtt{mostValue}(W,((w_0,v_0),\ldots,(w_{n-1},v_{n-1})))$ is the maximum amount of value that we can have given a budget of $W$ and weights/values $((w_0,v_0),\ldots, (w_n,v_n))$ then we have the following equation: # # $\mathtt{mostValue}(W,((w_0,v_0),\ldots,(w_{n-1},v_{n-1}))) = \max \{ 0 + \mathtt{mostValue}(W,((w_1,v_1),\ldots,(w_{n-1},v_{n-1}))) , v_0 + \mathtt{mostValue}(W-w_0,((w_1,v_1),\ldots,(w_{n-1},v_{n-1}))) \}$. # # Can you see why? # # This now suggests a simple recursive algorithm for the ```mostValue``` function: # In[4]: get_ipython().run_line_magic('run', " 'boaz_utils.ipynb'") # In[3]: def mostValue(W,L): """returns the maximum amount of value we can have with W kilos packing items listed in L, where L is a tuple/list containing pairs (w,v) weight/value for every item.""" step_pc() # ignore for now if not L: # if L is empty then we can't have any fun return 0 value_if_skip_first_item = mostValue(W,L[1:]) # the amount of value we can have if we skip first item if W=L[0]: # if n>=L[0], another option is using coin L[0] at least once result += countWays(L, n-L[0]) return result # In[40]: countWays([1,5,10,25,50,100],12) # Unfortunately this code can be quite slow. # In[41]: countWays([1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],10) # We can speed up the computation by using memoization. # In[42]: # how many ways are there to make change for n cents using only the coins in L[i:]? def countWaysMemo(L, val, mem): # base case: there are no coins to use if len(L) == 0: if val == 0: return 1 else: # if there are no coins you can use, it's impossible to make change return 0 elif mem[len(L)][val]!=-1: return mem[len(L)][val] else: # one option we have is not using coin L[0] at all mem[len(L)][val] = countWaysMemo(L[1:], val, mem) if val>=L[0]: # if n>=L[0], another option is using coin L[0] at least once mem[len(L)][val] += countWaysMemo(L, val-L[0], mem) return mem[len(L)][val] def countWaysFast(L, n): mem = [] for i in range(len(L)+1): mem += [[-1]*(n+1)] return countWaysMemo(L, n, mem) # In[43]: countWaysFast([1,5,10,25,50,100],12) # In[44]: countWaysFast([1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],10) # ### How fast is the memoized version? # # Let $m$ denote the length of the original list $L$. Then as we go through the recursion, then length of $L$ is always between $0$ and $m$, and $val$ is always between $0$ and $n$. Thus the total number of possible inputs to the memoized function is about $nm$. Each call only does $O(m)$ work (creating the list ```L[1:]``` takes $m$ steps), so in total the time is $O(nm^2)$. # # **Bonus**: think about how to make the runtime $O(nm)$ instead of $O(nm^2)$. A hint is to avoid having to create the list ```L[1:]```. What if instead we started with a recursive function ```countWays(i, L, n)``` which computes the number of ways to make $n$ cents using only the coins in ```L[i:]```, and then we memoized that?