Dynamic Programming We have looked at several algorithms that involve recursion. In some situations, these algorithms solve fairly difficult problems efficiently, but in other cases they are inefficient because they recalculate certain function values many times. The example given in the text is the fibonacci example. Recursively we have: public static int fibrec(int n) { if (n < 2) return n; else return fibrec(n-1)+fibrec(n-2); } The problem here is that lots and lots of calls to Fib(1) and Fib(0) are made. It would be nice if we only made those method calls once, then simply used those values as necessary. In fact, if I asked you to compute the 10th Fibonacci number, you would never do it using the recursive steps above. Instead, you'd start making a chart: F1 = 1, F2 = 1, F3 = 2, F4 = 3, F5 = 5, F6 = 8, F7 = 13, F8 = 21, F9 = 34, F10 = 55. First you calculate F3 by adding F1 and F2, then F4, by adding F3 and F4, etc. The idea of dynamic programming is to avoid making redundant method calls. Instead, one should store the answers to all necessary method calls in memory and simply look these up as necessary. Using this idea, we can code up a dynamic programming solution to the Fibonacci number question that is far more efficient than the recursive version: public static int fib(int n) { int[] fibnumbers = new int[n+1]; fibnumbers[0] = 0; fibnumbers[1] = 1; for (int i=2; i<n+1;i++) fibnumbers[i] = fibnumbers[i-1]+fibnumbers[i-2]; return fibnumbers[n]; } The only requirement this program has that the recursive one doesn't is the space requirement of an entire array of values. (But, if you think about it carefully, at a particular moment in time while the recursive program is running, it has at least n recursive calls in the middle of execution all at once. The amount of memory necessary to simultaneously keep track of each of these is in fact at least as much as the memory the array we are using above needs.) Usually however, a dynamic programming algorithm presents a time-space trade off. More space is used to store values, but less time is spent because these values can be looked up. Can we do even better (with respect to memory) with our Fibonacci method above? What numbers do we really have to keep track of all the time? public static int fib(int n) { int fibfirst = 0; int fibsecond = 1; for (int i=2; i<n+1;i++) { fibsecond = fibfirst+fibsecond; fibfirst = fibsecond - fibfirst; } return fibsecond; } So here, we calculate the nth Fibonacci number in linear time (assuming that the additions are constant time, which is actually not a great assumption) and use very little extra storage. To see an illustration of the difference in speed, I wrote a short main to test this: public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("Fib 30 = "+fib(30)); long mid = System.currentTimeMillis(); System.out.println("Fib 30 = "+fibrec(30)); long end = System.currentTimeMillis(); System.out.println("Fib Iter Time = "+(mid-start)); System.out.println("Fib Rec Time = "+(end-mid)); } // Output: // Fib Iter Time = 4 // Fib Rec Time = 258 Binomial Coefficients The binomial coefficients, the values in Pascal’s Triangle, are defined as follows: nC0 = nCn = 1 nCk = n-1Ck-1 + n-1Ck , for 0 < k < n It’s quite easy to see that a recursive method that calculates these values is as follows: public static int combo(int n, int k) { if (k == 0 || k == n) return 1; return combo(n-1, k-1) +combo(n-1, k); } Just like the Fibonacci numbers, this method does a lot of redundant work. We can avoid this work by storing the answer to each recursive call in a two dimensional array. In general, table[i][j] should store the answer to iCj. We know that we must initialize this array such that table[i][0] = 1, for all i table[i][i] = 1, for all i Once we do this, we can iterate through different locations in this array and calculate the necessary combinations. Here is the code: public static int combo(int n, int k) { int[][] table = new int[n+1][n+1]; for (int i=0; i<=n; i++) { table[i][0] = 1; table[i][i] = 1; } for (int i=2; i<=n; i++) for (int j=1; j<i; j++) table[i][j] = table[i-1][j-1] + table[i-1][j]; return table[n][k]; } The key in this solution is making sure we never do an array out of bounds, and also making sure that whenever we look up a value in table, it’s already been filled in. Subset Sum The subset sum problem is as follows: Given a set of positive integers, determine if any subset of them adds up to a given target. Here is a specific instance of the problem: S = {2, 19, 4, 22, 13, 48, 17} Target = 33 In this particular instance, there is no subset of S that adds up to 33. (You can get either 32 = 19 + 13, or 34 = 17 + 13 + 4, but you can’t hit 33 exactly.) Recursively, we might break the problem down by asking the following question: Does our subset that adds to 33 contain 17? If the answer is yes, then we must seek a subset that adds to 16 from the set {2, 19, 4, 22, 13, 48} If the answer is no, then we must seek a subset that adds to 33 from the set {2, 19, 4, 22, 13, 48}. Using the divide and conquer paradigm, this leads to a recursive solution. But, it’s very possible this recursive solution is inefficient and makes the same exact recursive calls multiple times. For each positive integer, n, the answer is either yes (there exists a subset that adds to n), or no (there is NO subset that adds to n). We can store the answers to each of these questions in an array of size n+1. (The valid indexes are 0 to n.) The value stored in index i of this array will be either true or false, representing whether or not there exists a sum that adds up to i, exactly. In the beginning of our algorithm, the only value for which we have a subset is 0. Thus, we set this entry to true, and all others to false. From there, we’ll consider each element in our set, one by one. We will simply ask the question, “Can this element help us create a subset that adds to some value k?” For example, if we are considering the value 2, and we want to know if it will help us create a subset that adds to 7, then we know the answer is YES if there already exists a subset that adds to 5. So, this is the key logic: if (subset[k - value]) subset[k] = true; Now, let’s consider turning this whole idea into code: public static int subsetsum(int[] values, int target) { boolean[] subset = new boolean[target+1]; subset[0] = true; for (int i=1; i<=target; i++) subset[i] = false; for (int i=0; i<values.length; i++) { for (int j=target; j>=values[i]; j--) { if (subset[j-values[i]]) subset[j] = true; } } return subset[target]; } The outer loop goes through each value in the set, one by one. The inner loop goes backwards (can you see why) and for each possible value, asks if we can form a subset of that value, with the help of the current element. If the answer is yes, we change the that entry in the subset array to true. Consider a short trace through this code with S = {3, 8, 2, 4} Target = 10 Our array starts as follows: 0 1 2 3 4 5 6 7 8 9 10 T F F F F F F F F F F When we consider the first value, 3, the only array value that will trigger to true will be index 3: 0 T 1 F 2 F 3 T 4 F 5 F 6 F 7 F 8 F 9 F 10 F This makes sense, because if our set only contained 3, the only subset we could create would add to 3. Now, consider 8, running through the code doesn’t change index 10 or 9 (because indexes 2 and 1 are false), but it does change index 8: 0 T 1 F 2 F 3 T 4 F 5 F 6 F 7 F 8 T 9 F 10 F Now, consider running through the loop with 2. It will trigger index 10, index 5 and index 2, to true: 0 T 1 F 2 T 3 T 4 F 5 T 6 F 7 F 8 T 9 F 10 T Notice that with the three values, 3, 8 and 2, we can create subsets that add to 2, 3, 5, 8 and 10. Now, consider the value 4: it will trigger index 9, 7, 6 and 4 to be true, since (5, 3, 2 and 0 are already true): 0 1 2 3 4 5 6 7 8 9 10 T F T T T T T T T T T This means that with our given set, there exists subsets that add to each value from 2 to 10.