More dynamic programming

Fibonacci and factorial

Last time, we saw an efficient implementation of a function to get the nth Fibonacci number:

def fib(n):
    fibs = [0, 1]
    i = 2
    while i <= n:
        fibs.append(fibs[i - 1] + fibs[i - 2])
        i = i + 1
    return fibs[n]

This solution finds the nth Fibonacci number by building a list of all of the Fibonacci numbers up to n. It stores each Fibonacci number in a list so that it can be used in the computation of subsequent Fibonacci numbers, avoiding recomputation.

We previously saw the Factorial function:

0! = 1
n! = n * (n - 1)! [when n > 0]

We could write a similar solution to factorial:

def factorial(n):
    factorials = [0]
    i = 1
    while i <= n:
        factorials.append(i * factorials[i - 1])
        i += 1
    return factorials[n]

Is this faster than the recursive version?

It’s not–in this case, we don’t have overlapping subproblems. When we compute n!, we’re only going to use it once: in the computation of (n+1)!.

Dynamic programming

Dynamic programming algorithms exploit overlapping subproblems by storing the solutions of subproblems so that they can be used for future solutions. So in order to define a dynamic programming algorithm, we need two components:

  1. Overlapping subproblems
  2. A way to build solutions to larger problems out of the solutions to smaller subproblems
  3. An ordering of the problems from smallest to largest so that solutions are always available when needed

For Fibonacci, we can compute a solution to Fib(n) from solutions to Fib(n - 1) and Fib(n - 2). That gives us (1). For (2), we need to make sure we compute Fib(n - 1) and Fib(n - 2) before we compute Fib(n). We can guarantee this by computing the Fibonacci numbers from 0 up to n (rather than from n down to 0).

Bills

Let’s say we’re operating a bank. We have bills of a number of denominations: $1, $2, $4, $5, $10, $20, etc. We have an essentially unlimited number of bills of each denomination. When a customer comes in and wants to make a withdrawal (of some arbitrary amount of money), we want to give them the least number of bills we can given the denominations we have at our disposal. Can we write an algorithm to do this?

Yes! We can use (as you might have guessed) dynamic programming. For a target amount A, the subproblems will be the problem solutions for amounts less than A. We can exploit our solutions to these subproblems as follows. Imagine that there are only two denominations: $1 and $5. Then, in order to produce the minimum set of bills for amount $A, we need to consider:

  • The minimum set of bills for $(A - 1), plus a $1 bill
  • The minimum set of bills for $(A - 5), plus a $5 bill

We should then pick whichever of these new sets of bills is smaller.

Next class, we’ll implement this solution in Python.

denominations = [1, 5, 10, 20, 50]

def bills(total):
    minimum_bills = [[]]
    i = 1
    while i <= total:
        best_bills = [1] * i
        for denomination in denominations:
            if denomination <= i:
                possible_bills = minimum_bills[i - denomination] + [denomination]
                if len(possible_bills) < len(best_bills):
                    best_bills = possible_bills
        minimum_bills.append(best_bills)
        i = i + 1
    return minimum_bills[total]