Memoization and Dynamic Programming

Warm-up: list exercises

In [1]:
# create a list of the numbers 0 through 9
data = [i for i in range(10)]
In [2]:
# create a list of the first 10 squares (0^2, 1^2, etc.)
[i*i for i in data]
Out[2]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [3]:
# create a list of the first 5, non-negative odd numbers
[i for i in data if i % 2 == 1]
Out[3]:
[1, 3, 5, 7, 9]
In [4]:
# create a list with 10 zeros
[0 for i in range(10)]
Out[4]:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Firstname Lastname
Th. 11/1

Recursive Fibonacci

Write a Python function that computes the $n^{\mathrm{th}}$ fibonacci number, where: $$ \begin{align*} \mathrm{fib}(1) &= 1 \\ \mathrm{fib}(2) &= 1 \\ \mathrm{fib}(n) &= \mathrm{fib}(n-1) + \mathrm{fib}(n-2) \end{align*} $$ for all $n > 0$.

In [5]:
def fib(n):
    '''
    Given a positive integer n, returns the nth fibonacci number, where
         fib(1) = fib(2) = 1
         fib(n) = fib(n-1) + fib(n-2)
    '''
    
    assert n > 0, 'fib requires a positive number'
    
    if (n == 1) or (n == 2):
        return 1
    
    return fib(n-1) + fib(n-2)
In [6]:
help(fib)
Help on function fib in module __main__:

fib(n)
    Given a positive integer n, returns the nth fibonacci number, where
         fib(1) = fib(2) = 1
         fib(n) = fib(n-1) + fib(n-2)

Measuring performance

In [7]:
fibTrials = range(1, 15)
iterations = 100
In [8]:
# collect some measurements
import experimental
recursiveFibResults = experimental.timeTrials(fib, fibTrials, iterations)
In [9]:
# plot the results
%matplotlib inline
experimental.plot(fibTrials, [recursiveFibResults], legend=['recursive'])

Aside: mutable default arguments

Mutability, as always, can hurt our brain.

Part 1: The (usually) wrong way to do mutable default arguments

In [10]:
def addElem(elem, l=[]):
    l.append(elem)
    return l
In [11]:
addElem(10, [0, 1, 2])
Out[11]:
[0, 1, 2, 10]
In [12]:
addElem(1)
Out[12]:
[1]
In [13]:
addElem(2)
Out[13]:
[1, 2]

Part 2: The (usually) right way to do mutable default arguments

Use the following idiom:

In [14]:
def addElem(elem, l=None):

    if not l:
        l = []
        
    l.append(elem)
    return l
In [15]:
addElem(10, [0, 1, 2])
Out[15]:
[0, 1, 2, 10]
In [16]:
myValues = []
addElem(1, myValues)
myValues
Out[16]:
[]
In [17]:
addElem(2, [])
Out[17]:
[2]

Memoization

Memoization saves time by caching the results of function calls.

In [18]:
def memo_fib(n, cache = {}):
    '''
    Given a positive integer n, returns the nth fibonacci number, where
         fib(1) = fib(2) = 1
         fib(n) = fib(n-1) + fib(n-2)
         
    This function memoizes the results.
    '''
    assert n > 0, 'fib requires a positive number'
    
    if (n == 1) or (n == 2):
        return 1
    
    if n not in cache:
        cache[n] = memo_fib(n-1) + memo_fib(n-2)
        
    return cache[n]
In [19]:
# take some measurements
memoizedFibResults = experimental.timeTrials(memo_fib, fibTrials, iterations)
In [20]:
# plot the results
experimental.plot(fibTrials, [recursiveFibResults, memoizedFibResults], legend=['recursive', 'memoized'])