DynProg01

advertisement
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.
Download