This is one of the simplest and cleanest dynamic programming problems.
Fibonacci numbers have a recursive definition:
#include <cstdlib> #include <iostream> using namespace std; class Fib { // Step 1 in calculating Fibonacci numbers with Dynamic Programming: public: // Find a recursive solution, which may be very slow. long long find_fib(long long n); }; long long Fib::find_fib(long long n) // Classic recursive implementation straight from the definition. { if (n == 0 || n == 1) return 1; return find_fib(n-1) + find_fib(n-2); } int main(int argc, char **argv) { Fib f; if (argc != 2) { cerr << "usage: fib n\n"; exit(1); } cout << f.find_fib(atoi(argv[1])) << endl; return 0; } |
The problem with this is that its performance blows up exponentially:
UNIX> bash UNIX> i=0 UNIX> while [ $i -lt 50 ]; do > echo $i `( time bin/fib1 $i ) 2>&1` # This squashes the output of "time" onto one line. > i=$(($i+1)) # You can ask me about it class or wait until CS360. > done 0 1 real 0m0.003s user 0m0.001s sys 0m0.001s 1 1 real 0m0.003s user 0m0.001s sys 0m0.001s # ....... Skipping lines 40 165580141 real 0m0.272s user 0m0.270s sys 0m0.001s 41 267914296 real 0m0.439s user 0m0.437s sys 0m0.001s 42 433494437 real 0m0.697s user 0m0.694s sys 0m0.001s 43 702028733 real 0m1.114s user 0m1.111s sys 0m0.001s 44 1134903170 real 0m1.812s user 0m1.809s sys 0m0.002s 45 1836311903 real 0m2.934s user 0m2.931s sys 0m0.002s 46 2971215073 real 0m4.756s user 0m4.752s sys 0m0.002s 47 4807526976 real 0m7.800s user 0m7.790s sys 0m0.005s 48 7778742049 real 0m13.135s user 0m13.103s sys 0m0.014s 49 12586269025 real 0m21.007s user 0m20.971s sys 0m0.016s UNIX>
However, with dynamic programming, we proceed to step two: memoization. We accept the recursive definition, but simply create a cache for the answers to that after the first time find_fib(i) is called for some i, it returns its answer from the cache. We implement it in src/fib2.cpp below. We initialize the cache with -1's to denote empty values.
class Fib { // This is step 2 of dynamic programming: Add a cache. public: long long find_fib(int n); vector <long long> cache; }; long long Fib::find_fib(int n) // Create the cache if this is the first call. { // Otherwise, return the answer from the cache if it's there. if (cache.size() == 0) cache.resize(n+1, -1); if (cache[n] != -1) return cache[n]; if (n == 0 || n == 1) { // If it's not in the cache, do the recursion, and put cache[n] = 1; // the answer into the cache before returning. } else { cache[n] = find_fib(n-1) + find_fib(n-2); } return cache[n]; } |
Now find_fib(n) is linear in n, which is a HUGE difference. The call that took 21 seconds below takes a couple of milliseconds:
UNIX> time bin/fib2 49 12586269025 real 0m0.003s user 0m0.001s sys 0m0.001s UNIX>
class Fib { // This is step three -- remove the recursion and build the cache. public: long long find_fib(int n); vector <long long> cache; }; long long Fib::find_fib(int n) { int i; if (n == 0 || n == 1) return 1; cache.resize(n+1, -1); /* Because all of our recursive calls were to smaller values of n, we can build the cache from small to big. */ cache[0] = 1; cache[1] = 1; for (i = 2; i <= n; i++) cache[i] = cache[i-1] + cache[i-2]; return cache[n]; } |
It's not appreciably faster, because fib2 is already smoking fast, but it does demonstrate step three:
UNIX> time bin/fib3 49 12586269025 real 0m0.003s user 0m0.001s sys 0m0.001s UNIX>
Keep this technique in mind -- sometimes you can do step 4 simply by looking at how you access the cache in step 3.
class Fib { // Step four: removing (or minimizing) the cache. public: long long find_fib(int n); }; long long Fib::find_fib(int n) { vector <long long> cache; int i; if (n == 0 || n == 1) return 1; /* The key observation here is that we only need the last two entries of the cache. So, we'll limit the cache to two entries. We'll keep the odd ones in cache[0], and the even ones in cache[1]. */ cache.resize(2, 1); for (i = 2; i <= n; i++) { cache[i%2] = cache[0] + cache[1]; } /* Return cache[0] if n is even, and cache[1] if n is odd. */ return cache[n%2]; } |