Dynamic Programming
In the previous lecture we discussed an inefficient way (recursive) to compute Fibonacci
numbers. The recursive algorithm of computing Fibonacci numbers can be classified as top-down divide-and-conquer
approach because it
- divides the initial problem into several smaller instances
- and it solves the problem with descending way from bigger problems to smaller ones.
The reason this algorithm was very inefficient was that the smaller instances fibonacci(n-1) and
fibonacci(n-2) are not really independent. In fact they both need fibonacci(n-3)
to compute. However, since the algorithm treats those instances as independent ones, we end up with computing values
for smaller Fibonacci numbers several times.
Dynamic programming is a technique that takes an opposite approach. It is similar to divide-and-conquer because it
also divides an instance into smaller instances. However, to solve those instances it takes the opposite direction solving
from the smallest one to the biggest and storing the results of smaller instances to reuse them later instead of recomputing
them. For example, the fibonacci() function to compute the n-th number can look like
unsigned int F[25000];
unsigned int fibonacci(int n)
{
for(int i=0;i<n;i++)
if( i < 2 ) F[i] = 1;
else F[i] = F[i-1] + F[i-2];
return F[n-1];
}
Of course, we can write this function in even more efficient way to avoid recomputing the same values if we need to call
the same function more than once:
unsigned int fibonacci(int n)
{
static int i = 0;
for(;i<n;i++)
if( i < 2 ) F[i] = 1;
else F[i] = F[i-1] + F[i-2];
return F[n-1];
}
Exercise:
Rewrite the following function for computing the
binomial coefficient according the dynamic approach:
unsigned int bin(int n, int k)
{
if( k==0 || n==k )
return 1;
return bin(n-1, k-1) + bin(n-1, k);
}
A binomial coefficient is defined by the following
| ( | n | ) = | n! | for 0≤k≤n |
| k | k! (n-k)! |
This function uses the following property of a binomial coefficient
Floyd's algorithm for shortest path*
A common problem encountered by air travelers is the determination of the shortest way from one city to another when a direct
flight does not exist. This problem can be visualized by a directed graph (or shortly digraph).
This graph is called directional graph because its edges have directions. In other words, we can use that way to get
from vertex 1 to vertex 4, but not back. Graphs like this are also weighted because each edge has a
weight associated with it. We cab think about those weight as prices or lengths of the way.
| W |
1 | 2 | 3 | 4 | 5 |
| 1 |
0 | 1 | ∞ | 1 | 5 |
| 2 |
9 | 0 | 3 | 2 | ∞ |
| 3 |
∞ | ∞ | 0 | 4 | ∞ |
| 4 |
∞ | ∞ | 2 | 0 | 3 |
| 5 |
3 | ∞ | ∞ | ∞ | 0 |
Those kind graphs can be represented by a square array where each element Wi,j contains
the weight of the directed edge from vertex i to vertex j. The matrix W is called the
adjacent matrix for the graph.
The Shortest Path problem is an optimization problem. There can be more than one candidate solution to an instance
of an optimization problem.Each candidate solution has a value associated with it, and a solution to the instance is any
candidate solution that has the optimal value. In the case of the Shortest Path problem, a candidate solution
is a path from one vertex to another, the value is the length of the path, and the optimal value is the
minimum of these lengths.
Using dynamic programming technique, we develop a cubic-time algorithm for the Shortest Path problem. Our first step would
be to create a square matrix D where each element di, j contains the length of the
shortest path from vertex i to vertex j. To create such a matrix, we will compute
sequence of matrices D(k) where 0≤k≤N (N is the number of vertices in the graph). Each element
d(k)i, j = the length of a shortest path from
vertex i to vertex j using only vertices with numbers less or equal k
as intermediate vertices.
For example, for the graph on the picture some values of these matrices are:
| d(0)2, 5 |
= length[v2, v5] = ∞ |
| d(1)2, 5 |
= min( length[v2, v5], length[v2, v1, v5]) = min(∞, 14) = 14 |
| d(2)2, 5 |
= 14 (For any graph a shortest path starting at vertex 2 cannot pass through that vertex.) |
| d(3)2, 5 |
= 14 (For this graph including vertex 3 yields no new paths from vertex 2 to vertex 5.) |
| d(4)2, 5 |
= min(length[v2, v1, v5],
length[v2, v4, v5],
length[v2, v1, v4, v5],
length[v2, v3, v4, v5]) = min(14, 5, 13, 10) = 5 |
| d(5)2, 5 |
= 5 (For any graph a shortest path ending at vertex 5 cannot pass through that vertex.) |
The general idea of Floyd's algorithm is to compute that sequence of matrices D(k). Obviously
D(0) = W and D(N) = D - the final matrix we are interested in. Thus, all we need to do
according to the dynamic programming technique is to develop a way to compute matrix D(k) based on the
previous matrix D(k-1). We can accomplish this by considering two different cases:
- At least one shortest path from vertex i to vertex j, using only vertices
with indexes less or equal k as intermediate vertices, does not use vertex k.
In this case d(k)i, j
= d(k-1)i, j
- All shortest paths from vertex i to vertex j, using only vertices
with indexes less or equal k as intermediate vertices, do use vertex k.
In this case
d(k)i, j
= d(k-1)i, k
+ d(k-1)k, j
Thus, the problem of generating that short distances matrix D can be solved by the following function:
void floyd(int W[][], int D[][], int N)
{
int i, j, k;
// initially we know only about direct connections
for(i=0;i<N;i++)
for(j=0;j<N;j++)
D[i][j] = W[i][j];
// we start updating the matrix D here
for(k=0;k<N;k++)
for(i=0;i<N;i++)
for(j=0;j<N;j++)
if( D[i][k]!= INFINITY &&
D[k][j]!= INFINITY &&
D[i][k]+D[k][j]<D[i][j]
)
D[i][j] = D[i][k]+D[k][j];
}
Please find the complete code on the example page.
Exercise:
The algorithm above provides us with the information about weights of the shortest paths, but it doesn't tell us anything
about the paths themselves. You assignment is to modify the algorithm to keep all the vertices for each shortest path found.
That is, the modified function should also return an array
V, which has both its rows and columns indexed
from 0 to
N-1, where
| V[i][j] = |
highest index of an intermediate vertex on the shortest path from vertex i
to vertex j, if at least one intermediate vertex exists, or zero, otherwise.
|
If we have the array
V with this information we can relatively easy find out all vertices on the way from
vertex
i to vertex
j. To do this, we need to look at the vertex
V[i][j]
and if this is not zero, we need to find paths from
i->
V[i][j] and
V[i][j]->
j. The function that prints the complete path from vertices
i
and
j can look like
void print_shortest_path(int i, int j)
{
if( V[i][j] != 0 ){
print_shortest_path(i, V[i][j]);
cout<<"->"<<V[i][j];
print_shortest_path(V[i][j], j);
}
}
Knapsack problem
Imagine you are a thief breaking into a jewelry store carrying a knapsack. The knapsack will break if the total weight of
the items stolen exceeds some maximum weight W. Each item has a value and a weight. The thief's dilemma is to
maximize the total value of the items while not making the total weight exceeds W. This problem is called
0-1 Knapsack problem.
The formal definition of the problem is: Let
S = {item1, item2, ..., itemn}
wi = weight of itemi
pi = profit of itemi
W = maximum weight the knapsack can hold,
where wi, pi, and W are positive integers. Determine a
subset A of S such that
| ∑ pi |
is maximized subject to |
∑ wi |
≤ W |
| itemi∈ A |
itemi∈ A |
To apply the dynamic programming principle, we need to show that this problem can be considered as divide-and-conquer
problem. In other words, we need to define the problem through itself of a smaller size. To that end, let A
be an optimal subset of the n items. There are two cases:
- either A does not contains itemn, and in this case A is equal to an
optimal subset of the first n-1 items;
- or A does contains itemn, and in this case the total profit of the items in
A is equal to pn plus the optimal profit obtained when the items can be chosen
from the first n-1 items under the restriction that the total weight cannot exceed
W-wi
This can be generalized as follows. For k>0 and w>0, let P[k][w] be the optimal profit obtained
when choosing items only from the first k items under the restriction that the total weight cannot exceed w
| P[k][w] = |
{ |
maximum(P[k-1][w], pi+P[k-1][w-wi])
|
if wi≤w
|
| P[k-1][w] |
if wi>w
|
For example, if we are solving problem shown on the picture, we have three items and the total weight W =30. That
is,we need to find P[3][30]. According the the formula above, to compute this, we need to know
P[3-1][30] = P[2][30]
and P[3-1][30-w3] = P[2][10]
To compute P[2][3], we need
P[2-1][30] = P[1][30]
and P[2-1][30-w2] = P[1][20]
To compute P[2][10], we need
P[2-1][10] = P[1][30]
and P[2-1][10-w2] = P[1][0]
Since we are using the dynamic programming, we are going from bottom up; that is, we will start with computing P[1][.]. According to the recursive formula, we have: P[0][.] = 0,
P[1][0] = $0, P[1][10] = $50, P[1][20] = $50,
P[1][30] = $50.
Now we are ready to compute the second row:
| P[2][10] = |
{ |
maximum(P[1][10], $60 + P[1][10])
|
if w2=10≤30
|
= $60 |
| P[1][10] |
if w2=10>30
|
| P[2][30] = |
{ |
maximum(P[1][30], $60 + P[1][20])
|
if w2=10≤30
|
= $60 + $50 = $110 |
| P[1][30] |
if w2=10>30
|
And finally, we compute the last row
| P[3][30] = |
{ |
maximum(P[2][30], $140 + P[2][10])
|
if w3=20≤30
|
= $140 + $60 = $200 |
| P[2][30] |
if w3=20>30
|
You can find the recursive solution of the knapsack problem on the
example page.