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:
- Overlapping subproblems
- A way to build solutions to larger problems out of the solutions to smaller subproblems
- 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]