#!/usr/bin/env python # coding: utf-8 # # More memoization # # Example: number of ways to sum up to an integer # # Let ```numWays(n)``` be the number of ways to write a nonnegative integer ```n``` as the sum # of positive integers. For example, there are ```8``` ways of writing ```4```: ```1 + 1 + 1 + 1, 2 + 1 + 1, 1 + 2 + 1, 1 + 1 + 2, 2 + 2, 1 + 3, 3 + 1```, and ```4```. One can show by induction that ```numWays(n)``` = $2^{n−1}$ , but let’s see how to calculate it using recursion and memoization. # ### Recursive implementation without memoization # In[8]: def numWays(n): if n==0: return 1 ans = 0 for i in range(1, n+1): # try the first number in sum being i, then the remaining part must sum to n-i ans += numWays(n-i) return ans # ### With memoization # In[9]: def memNumWays(n, seen, mem): if n==0: return 1 elif seen[n]: return mem[n] seen[n] = True mem[n] = 0 for i in range(1, n+1): mem[n] += memNumWays(n-i, seen, mem) return mem[n] def numWaysFast(n): seen = [False]*(n+1) mem = [0]*(n+1) return memNumWays(n, seen, mem) # In[10]: numWays(4) # In[11]: numWays(14) # In[15]: numWays(24) # In[16]: # now using the memoized version numWaysFast(24) # ### Another example # # What if we want to compute a function distinctNumWays(n) which doesn’t differentiate between different orderings of the same sum? For example, it treats ```1 + 1 + 2``` and ```2 + 1 + 1``` as the same sum. So, there would only be ```5``` ways to sum up to the number ```4```: ```1 + 1 + 1 + 1, 1 + 2 + 2, 2 + 2, 1 + 3, 4```. # # We can calculate ```distinctNumWays(n)``` recursively as well, by generating all ways of forming ```n``` where the integers in the sum are generating in nondecreasing order. That is, we would not generate ```2 + 1 + 1``` or ```1 + 2 + 1``` since the integers do not appear in nondecreasing order; we would only generate ```1 + 1 + 2```. That way, we never count each sum exactly once. # In[19]: # how many ways are there to sum up to n, not counting different # orderings of the sum, when the smallest number must be at least # atLeast def recurse(n, atLeast): if n==0: return 1 ans = 0 for i in range(atLeast, n+1): ans += recurse(n-i, i) return ans def distinctNumWays(n): return recurse(n, 1) # ### Now with memoization # In[35]: def recurseMem(n, atLeast, seen, mem): if n==0: return 1 elif seen[n][atLeast]: return mem[n][atLeast] seen[n][atLeast] = True mem[n][atLeast] = 0 for i in range(atLeast, n+1): mem[n][atLeast] += recurseMem(n-i, i, seen, mem) return mem[n][atLeast] def distinctNumWaysFast(n): mem = [[-1 for x in range(n+1)] for y in range(n+1)] seen = [[False for x in range(n+1)] for y in range(n+1)] return recurseMem(n, 1, seen, mem) # In[21]: distinctNumWays(4) # In[31]: distinctNumWays(60) # In[36]: distinctNumWaysFast(60) # ### Last example: figuring out how to have the most fun at parties # # 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 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. Yesterday we asked: is the maximum amount of fun you can have with your budget? # # Today we will ask the question: what if you want to know specifically which parties to attend to have the most fun while respecting the budget constraint? # In[48]: def maximum_fun_recur2(D,L,seen,mem,choices): """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.""" if seen[D][len(L)]: return (mem[D][len(L)],choices) if len(L)==0: # if L is empty then we can't have any fun seen[D][0] = True mem[D][0]=0 return (0,choices) seen[D][len(L)] = True fun_if_skip_first_party,c = maximum_fun_recur2(D,L[1:],seen,mem,choices) # the amount of fun we can have if we skip first party if D L[0][1]+fun_if_attend_first_party: mem[D][len(L)] = fun_if_skip_first_party choices[D][len(L)] = 'D' else: mem[D][len(L)] = L[0][1]+fun_if_attend_first_party choices[D][len(L)] = 'G' # 'G' means "go" to first party return (mem[D][len(L)], choices) def memoized_maximum_fun2(D,L): seen = [[False]*(len(L)+1) for i in range(D+1)] mem = [[-1]*(len(L)+1) for i in range(D+1)] choices = [[-1]*(len(L)+1) for i in range(D+1)] return maximum_fun_recur2(D,L,seen,mem,choices) def whichParties(D, L): best,choices = memoized_maximum_fun2(D, L) parties = [] while len(L) > 0: c = choices[D][len(L)] if c == 'G': parties += [L[0]] D -= L[0][0] L = L[1:] return parties # In[49]: whichParties(4,((1,6),(2,5),(3,6),(2,10))) # ### Exercise 1 # # Remember in yesterday's exercises, you were given an arithmetic expression with digits separated by `*` and `+` and were asked: how can you parenthesize the expression so as to maximize its value? For example, with the expression $1+2*3+4*5$ the best way of parenthesizing it is $(1+2)*((3+4)*5)$, giving $105$. For example, parenthesizing it as $1+((2*3)+(4*5))$ would only give $27$. # # In today's lab, implement a function ```withParens(s)``` which outputs the string ```s``` parenthesized in a way that achieves the maximum value. For example, ```withParens```(`'1+2*3+4*5'`) should return `'(1+2)*((3+4)*5)'` # In[15]: def withParens(s): # write your code here pass # ### Exercise 2 # # Consider the function ```makeChange(n,L)``` from yesterday's lab, which returned the minimum number of coins needed from the list of denominations ```L``` to make change for ```n``` cents. Write a function ```whichCoins(n, L)``` which actually returns a list of coins used to make change in this optimal way. # In[ ]: def whichCoins(n,L): # write your code here pass