#!/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. # We will show how we can use memoization to make significant savings in running time. # # # Example1: Fun at Parties # # You have a budget of $D$ dollars. You are given a list $L$ of parties $[ [c_0,f_0],\ldots, [c_{n-1},f_{n-1}]]$ where the $i^{th}$ party costs $c_i$ to attend and will give you $f_i$ units of fun. # What is the maximum amount of fun you can have with your budget? # 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 party that has cost $c$ and yields fun $f$, then our decision is simple: if $D \geq c$ then we can attend the party and get $f$ units of fun, and otherwise we get zero units of fun. # # * If there are two parties with costs/fun $(c_0,f_0)$ and $(c_1,f_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 attend the first party. If we do attend it, then we'll get $f_0$ units of fun, and be left with a budget of $D-c_0$. If we don't then we get zero units of fun, and are left with $D$ dollars. # # * Therefore if $\mathtt{maximum\_fun}(D,((c_0,f_0),\ldots,(c_n,f_n)))$ is the maximum amount of fun that we can have given a budget of $D$ and a the cost/funs $((c_0,f_0),\ldots, (c_n,f_n))$ then we have the following equation: # # $\mathtt{maximum\_fun}(D,((c_0,d_0),\ldots,(c_n,f_n))) = \max \{ 0 + \mathtt{maximum\_fun}(D,((c_1,d_1),\ldots,(c_n,f_n))) , f_0 + \mathtt{maximum\_fun}(D-c_0,((c_1,d_1),\ldots,(c_n,f_n))) \}$. # # Can you see why? # # This now suggests a simple recursive algorithm for the '''maximum_fun''' function: # In[80]: def maximum_fun(D,L): """returns the maximum amount of fun we can have with D dollars attending the parties listed in L, where L is a tuple/list containing pairs (c,f) of cost/fun for every party.""" step_pc() # ignore for now if not L: # if L is empty then we can't have any fun return 0 fun_if_skip_first_party = maximum_fun(D,L[1:]) # the amount of fun we can have if we skip first party if Dmemoization in order to speed up ```max_paren```. # In[14]: # returns the maximum value that can be obtained by parenthesizing s[a:b] def max_paren_memoized(a, b, s, seen, mem): #write your code here pass def max_paren_fast(s): seen = [[[False] for x in range(len(s)+1)] for y in range(len(s)+1)] mem = [[[0] for x in range(len(s)+1)] for y in range(len(s)+1)] return max_paren_memoized(0, len(s), s, seen, mem) # ### Exercise 2 # # Suppose you live in a country whose coin denominations are in the list ```L```. For example, in Ethiopia we would have ```L = [1,5,10,25,50,100]```, but other countries have different coin systems. Implement a function ```makeChange(n, L)``` which returns the minimum number of coins needed to make change for ```n``` cents using the coins in ```L```. For example, ```makeChange(14, [1,5,10,25,50,100])``` should return ```5```, since you can make change for ```14``` cents by giving one ```10```-cent piece and four ```1```-cent pieces, for a total of ```5``` coins (you could also give two ```5```-cent pieces and four ```1```-cent pieces, or ```14``` ```1```-cent pieces, but those options would each require more coins). If it is impossible to make change for ```n``` cents using ```L```, you should return ```-1```. For example, ```makeChange(3, [2, 5])``` should return ```-1``` since there is no way to make change for ```3``` cents using ```2```-cent and ```5```-cent pieces. # # First implement ```makeChange``` using plain recursion. Then implement a faster version using memoization. # In[15]: def makeChange(n, L): # write your code here pass # In[ ]: print makeChange(14, [1,5,10,25,50,100]) # should print 5 print makeChange(3, [2,5]) # should print -1 print makeChange(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 # ### Exercise 3 # # Write a function ```lis(L)``` which takes as input a list of integers ```L``` and outputs the length of the longest increasing subsequence (lis) of ```L```. A subsequence of ```L``` is a sublist of ```L``` that does not have to be contiguous. For example, ```[1,5,9]``` is a subsequence of the list ```L = [1,2,3,4,5,6,7,8,9]``` since ```1,5,9``` appear in the list ```L``` in the same order (though just not in a row). ```9,5,1``` is not a subsequence of ```L``` since it does not appear in ```L``` in that order. # # First implement ```lis``` using plain recursion, then implement a faster version using memoization. # In[ ]: def lis(L): # write your code here pass # In[ ]: print lis([1,2,3,4,5,6,7,9]) # should print 9, since the entire list is an increasing sequence print lis([5,6,7,1,2,3,4]) # should print 4, since the LIS is 1,2,3,4 print lis([5,1,6,2,7,3,4]) # should print 4, since the LIS is still 1,2,3,4