Data Structures and Algorithms Lecture Notes: Algorithm Paradigms: Dynamic Programming
Data Structures and Algorithms Lecture Notes: Algorithm Paradigms: Dynamic Programming
7 mai 2021
Outline
function Fib(n)
if (n ≤ 1) then return n ;
else
return Fib(n − 1) + Fib(n − 2)
Computing Fibonacci numbers
function Fib(n)
if (n ≤ 1) then return n ;
else
return(Fib(n − 1) + Fib(n − 2)) ;
function Fib(n)
if (n ≤ 1) then return n ;
else
return Fib(n − 1) + Fib(n − 2) ;
function Fib(n)
if (n ≤ 1) then return n ;
else
return Fib(n − 1) + Fib(n − 2) ;
One possible recurrence relation for this algorithm that counts the
number of time the function Fib is called is the following :
1 if n=0
T(n) = 1 if n=1
T(n − 1) + T(n − 2) + 1 if n > 1
√
The solution of this recurrence is O(( 1+2 5 )n ), the time complexity is
exponential.
Call tree of D&C Fibonacci
function Fib(n)
if (n ≤ 1) then return n ;
else
return Fib(n − 1) + Fib(n − 2) ;
The poor time complexity of Fib derived from re-solving the same
sub-problems several times.
Save the solution to a subproblem in a table (an array), and refer back
to the table whenever we revisit the subproblem
Dynamic programming main ideas
I here computing Fib(4) and Fib(5) both require Fib(3), but Fib(3)
is computed only once
function DyFib(n)
if (n == 0) return 0 ;
if (n == 1) return 1 ;
if (table[n] != 0) return table[n] ;
else
table[n] = DyFib(n − 1) + DyFib(n − 2)
return table[n] ;
input 0 1 2 3 4 5 6 7 8 9 10 11 12 13
solution 0 1
Bottom up dynamic programming for Fibonacci
Bottom up design is an iterative algorithm which first compute the
base cases and then uses the solutions to the base cases to start
computing the solutions to the other larger subproblems :
function fib dyn(n)
int *table, i ;
table = malloc((n + 1) ∗ sizeof(int)) ;
for (i = 0; i ≤ n; i + +)
if (i ≤ 1)
table[i] = i ;
else
table[i] = table[i − 1] + table[i − 2] ;
return f [n] ;
input 0 1 2 3 4 5 6 7 8 9 10 11 12 13
solution 0 1
√
fib dyn ∈ Θ(n) as opposed to the exponential complexity O(( 1+2 5 )n )
for fib rec.
When do we need DP
For example, what is the smallest amount of coins needed to pay back
$2.89 (289 cents) using as denominations ”one dollars”, ”quaters”,
”dimes” and ”pennies”.
How did we get this D&C algo ? The making change problem has 2 input
parameters :
1. n the number of different denominations
2. N the amount of change to return
Sub-problems are obtained by reducing the value of one of the two inputs :
1. Try to solve a sub-problem using n − 1 denominations, i.e. the sub-problem
make change(i − 1, j)
2. Use denomination n to reduce the amount of money to return, thus we are left
with the sub-problem make change(i, j − di ) to solve.
The base case is when the amount to return is 0, in which case no coin is used, the
solution to the base case is 0.
C code implementing recursive make change
#include <stdio.h>
#define min(a,b)((a<b)? a:b)
int make_change(int d[], int n, int N)
{
if(N == 0) return 0;
else if (N < 0 || (N > 0 && n <= 0)) return 1000;
else{
return min(make_change(d,n-1,N), make_change(d,n,N-d[n-1]) + 1);
}
}
int main()
{
int d[] = {1, 5, 10, 25};
int N = 13;
int n = sizeof(d)/sizeof(d[0]);
int ans = make_change(d, n, N);
printf("Minimal # of coins = %d\n",ans);
return 0;
}
Designing DP based on the D&C algo
The D&C algo has two parameters because a make change problem has two
dimensions : number of different types of coins available and the amount to return.
An instance of make change problem can be uniquely identified by these two values.
DP uses a table to store the values of instances that have already been computed, it
makes sense here to use a 2-dimensional table where one dimension refers to the
coin types and the other to the amounts to return
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1
d2 = 4
d3 = 6
The entries of the above table can store the values of all the sub-problems of a
make change problem instance with 3 types of coins (1,4,6) and an amount to
return equal to 8.
For example, entry (d1 = 1, 2) refers to a sub-problem where the amount to return
is 2 and where only coins of type d1 = 1 are available to return this amount.
Designing DP based on the D&C algo
To solve this problem by dynamic programming we set up a table t[1..n, 0..N], one
row for each denomination and one column for each amount from 0 unit to N units.
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1
d2 = 4
d3 = 6
Entry t[i, j] will store the solution to sub-problem instance i, j, i.e. the minimum
number of coins needed to refund an amount of j units using only coins from
denominations 1 to i.
Designing DP based on the D&C algo
one can immediately fill the 3 entries of the table where j = 0, i.e. for
i = 1, 2, 3
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0
d2 = 4 0
d3 = 6 0
Designing DP based on the D&C algo
The solution to make change for instances where i = 0 is not defined. Thus the DP
for computing the entries of instances where i = 1 can only be based on the second
recursive call of the D&C algo :
function Make Change(i,j)
if (j == 0) then return 0 ;
else
return min(make change(i − 1, j), make change(i, j − di ) + 1) ;
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0
d3 = 6 0
Example : the value stored in entry t[1, 4] is interpreted as the minimum number of
coins to return 4 units using only denomination 1, which is the minimum number of
coins to return 3 units, i.e. t[1, 3] + 1 = 4 coins.
Designing DP based on the D&C algo
For the solutions of instances where i > 1, one needs to consider both recursive calls
of the D&C algo. and take the minimum value return from these two.
function Make Change(i,j)
if (j == 0) then return 0 ;
else
return min(make change(i − 1, j), make change(i, j − di ) + 1) ;
Thus the solution to instance t[i, j] is the minimum of the values returned by
make change(i − 1, j) and make change(i, j − di ) + 1, i.e. t[i, j] =
min(t[i − 1, j], t[i, j − di ]).
Special case : if the amount of change to return is smaller than domination di , i.e.
j < di , then the change needs to be returned can only be based on denominations
smaller than di , i.e t[i, j] = t[i − 1, j]
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0 1 2 3
d3 = 6 0 1 2 3 1 2
A bottom-up DP algorithm for making change
For the general, the following table look-ups deduced from the D&C algo is used to
compute entries in the DP table :
DP making change(n, N)
int d[1..n] = d[1, 4, 6] ;
int t[1..n, 0..N] ;
for (i = 1; i ≤ n; i + +) t[i, 0] = 0 ; */base case */
for (i = 1; i ≤ n; i + +)
for (j = 1; j ≤ N; j + +)
if (i == 1) then t[i, j] = t[i, j − di ] + 1
else if (j < d[i]) then t[i, j] = t[i − 1, j]
else t[i, j] = min(t[i − 1, j], t[i, j − d[i]] + 1)
return t[n, N] ;
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0 1 2 3 1 2 3 4 2
d3 = 6 0 1 2 3 1 2 1 2 2
Entry t[n, N] display the minimum number of coins that can used to return change
for N.
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0 1 2 3 1 2 3 4 2
d3 = 6 0 1 2 3 1 2 1 2 2
From other entries in the DP table we can also find what is the denomination of the
coins to return :
I Start at entry t[n, N] ;
I If t[i, j] = t[i − 1, j] then no coin of denomination i has been used to calculate
t[i, j], then move to entry t[i − 1, j] ;
I If t[i, j] = t[i, j − di ] + 1, then add one coin of denomination i and move to
entry t[i, j − di ].
Exercises 1 and 2
1. Construct the table and solve the making change problem where
n = 3 with denominations d1 = 1, d2 = 2 and d3 = 3 where the
amount of change to be returned is N = 7
2. Construct the table and solve the making change problem where
n = 4 with denominations d1 = 1, d2 = 3, d3 = 4 and d4 = 5
where the amount of change to be returned is N = 12
Solutions table
Amount 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0 1 2 3 1 2 3 4 2
d3 = 6 0 1 2 3 1 2 1 2 2
There is only one constraint : the sum of the value of the coins is equal
to the amount to be returned
Optimal Substructure
Coins/Amounts 0 1 2 3 4 5 6 7 8
d1 = 1 0 1 2 3 4 5 6 7 8
d2 = 4 0 1 2 3 1 2 3 4 2
d3 = 6 0 1 2 3 1 2 1 2 2
Often we start with all optimal subsolutions of size 1, then compute all
optimal subsolutions of size 2 combining some subsolutions of size 1.
We continue in this fashion until we have the solution for n.
Given n objects with integer weights wi and values vi , you are asked to
pack a knapsack with no more than W weight (W is integer) such
that the load is as valuable as possible (maximize). You cannot take
part of an object, you must either take an object or leave it out.
Given
I n integer weights w1 , . . . , wn ,
I n values v1 , . . . , vn , and
I an integer capacity W ,
assign either 0 or 1 to each of x1 , . . . , xn so that the sum
n
X
f (x) = xi vi
i=1
is maximized, s.t.
n
X
xi wi ≤ W .
i=1
Explanation
The value of the chosen load is ni=1 xi vi . We want the most valuable
P
load, so we want to maximize this sum.
The weight of the chosen load is ni=1 xi wi . We can’t carry more than
P
W units of weight, so this sum must be ≤ W .
Solving the 0-1 Knapsack
Claim :
If {x1 , x2 , . . . , xk } is an optimal solution to the knapsack problem with
weight W , then {x1 , x2 , . . . , xk−1 } is an optimal solution to the knapsack
problem with W 0 = W − wxk .
Optimal Substructure
Proof : Assume {x1 , x2 , . . . , xk−1 } is not an optimal solution to the
subproblem. Then there are objects {y1 , y2 , . . . , yl } such that
and
vy1 + vy2 + · · · + vyl > vx1 + vx2 + · · · + vxk−1 .
Then
The base case will be when one object is left to consider. The solution
is
v1 if w1 ≤ j
K [1, j] =
0 if w1 > j.
Once the value of the base case is computed, the solution to the other
subproblems is obtained as followed :
K [i − 1, j] if wi > j
K [i, j] =
max(K [i − 1, j], K [i − 1, j − wi ] + vi ) if wi ≤ j.
int K(i, W )
if (i == 1) return (W < w[1]) ? 0 : v[1]
if (W < w [i]) return K(i − 1, W ) ;
return max(K(i − 1, W ), K(i − 1, W − w [i]) + v [i]) ;
i 1 2 3 4 5
wi 6 5 4 2 2
vi 6 3 5 4 6
i 1 2 3 4 5
wi 6 5 4 2 2
int K(i, W ) vi 6 3 5 4 6
if (i == 1) return (W < w[1]) ? 0 : v[1] ;
if (W < w [i]) return K(i − 1, W ) ;
return max(K(i − 1, W ), K(i − 1, W − w [i]) + v [i]) ;
5 16 10
4 11 10 4 10 8
3 11 10 3 6 8 3 6 8 3 6 6
2 6 10 2 6 6 2 6 8 2 0 4 2 6 8 2 0 4 2 6 6 2 0 2
1 6 10 1 0 5 1 6 6 1 0 1 1 6 8 1 0 3 1 0 4 1 6 8 1 0 3 1 0 4 1 6 6 1 0 1 1 0 2
C code implementing recursive 0-1 knapsack
The initial call to K (n − 1, W ) because array indexes in C start at 0,
so values of object 1 are in val[0] and wt[0], etc.
#include <stdio.h>
int max(int a, int b) { return (a > b) ? a : b; }
int K(int W, int wt[], int val[], int n) {
// Base Case
if (n ==0) return (W < wt[0])? 0 : val[0];
//Knapsack does not have residual capacity for object n
if (wt[n] > W) return K(W, wt, val, n - 1);
else
return max(
val[n] + K(W - wt[n], wt, val, n - 1),
K(W, wt, val, n - 1));
}
int main() {
int val[] = { 6, 3, 5, 4, 6}; int wt[] = { 6, 5, 4, 2, 2 };
int W = 10;
int n = sizeof(val) / sizeof(val[0]);
printf("The solution is %d\n", K(W, wt, val, n-1));
return 0;
}
Analysis of the Recursive Solution
T (1) = 1.
Thus, if nW < 2n , then the 0-1 knapsack problem will certainly have
overlapping subproblems, therefore using dynamic programming is
most likely to provide a more efficient algorithm.
i\j 0 1 2 3 4 5 6 7 8 9 10
1
2
3
4
5
6
0-1 Knapsack : Bottom up DP algorithm
Initialization of the table using the base case of the recursive function :
if (i == 1) return (W < w[1]) ? 0 : v[1]
This said that if the capacity is smaller than the weight of object 1,
then the value is 0 (cannot add object 1), otherwise the value is v [1]
i\j 0 1 2 3 4 5 6 7 8 9 10
1 0 0 0 7 7 7 7 7 7 7 7
2 0
3 0
4 0
5 0
6 0
0-1 Knapsack : Bottom up DP algorithm
The DP code for computing the other entries of the table is based on
i 1 2 3 4 5 6
the recursive function for 0-1 knapsack : wi 3 2 6 1 7 4
vi 7 10 2 3 2 6
int K(i, W )
if (i == 1) return (W < w [1]) ? 0 : v [1] ;
if (W < w [i]) return K(i − 1, W ) ;
return max(K(i − 1, W ), K(i − 1, W − w [i]) + v [i]) ;
i\j 0 1 2 3 4 5 6 7 8 9 10
1 0 0 0 7 7 7 7 7 7 7 7
2 0
3 0
4 0
5 0
6 0
0-1 Knapsack : Bottom up DP algorithm
The bottom-up dynamic programming algorithm is now (more or less)
straightforward.
function 0-1-Knapsack(w , v , n, W )
int K[n, W + 1] ;
for(i = 1; i ≤ n; i + +) K [i, 0] = 0 ;
for(j = 0; j ≤ W ; j + +)
if (w [1] ≤ j) then K [1, j] = v [1] ;
else K [1, j] = 0 ;
for (i = 2; i ≤ n; i + +)
for (j = 1; j ≤ W ; j + +)
if (j ≥ w [i] && K [i − 1, j − w [i]] + v [i] > K [i − 1, j])
K [i, j] = K [i − 1, j − w [i]] + v [i] ;
else
K [i, j] = K [i − 1, j] ;
return K[n, W ] ;
Caution on the running time of the DP algo for knapsack
The previous algorithm runs in O(nW ), this seems polynomial in the input size, but
this is not the case because W is not polynomial
Two of the inputs of the previous algo are a vector of n weights and a vector of n
values. Assume the largest number in these two vectors is 29, thus we need max 5
bits to represent any number in the two vectors. Thus the total number of input bits
to represent these two vectors 2 × 5 × n. The outer for loop for
(i = 2; i ≤ n; i + +) runs n times which is linear in the size of the two input vectors
The other input is W . Assume W = 16, thus we need only 4 bits to represent W .
However, the inner loop for (j = 1; j ≤ W ; j + +) runs 16 times, i.e. 24 times its
input size of 4 bits !
For this reason, the running time of DP is said to be ”pseudo-polynomial”. Knapsack
is a NP-hard problem (actually weakly NP-hard), which means it is unlikely to have
a polynomial time solution
for (i = 2; i ≤ n; i + +)
for (j = 1; j ≤ W ; j + +)
if (j ≥ w [i] && K [i − 1, j − w [i]] + v [i] > K [i − 1, j])
K [i, j] = K [i − 1, j − w [i]] + v [i] ;
else
K [i, j] = K [i − 1, j] ;
i\j 0 1 2 3 4 5 6 7 8 9 10
1 0 0 0 7 7 7 7 7 7 7 7
2 0 0 10 10 10 17 17 17 17 17 17
3 0 0 10 10 10 17 17 17 17 17 17
4 0 3 10 13 13 17 20 20 20 20 20
5 0 3 10 13 13 17 20 20 20 20 20
6 0 3 10 13 13 17 20 20 20 23 26
Finding the Knapsack
With this problem, we don’t have to keep track of anything extra. Let
K [n, k] be the maximal value.
i\j 0 1 2 3 4 5 6 7 8 9 10
1 0 0 0 7 7 7 7 7 7 7 7
2 0 0 10 10 10 17 17 17 17 17 17
3 0 0 10 10 10 17 17 17 17 17 17
4 0 3 10 13 13 17 20 20 20 20 20
5 0 3 10 13 13 17 20 20 20 20 20
6 0 3 10 13 13 17 20 20 20 23 26
int K(i, W )
if (i == 1) return (W < w[1]) ? 0 : v[1] ; i 1 2 3 4 5
if (W < w [i]) return K(i − 1, W ) ; wi 6 5 4 2 2
return max(K(i − 1, W ), K(i − 1, W − w [i]) + v [i]) ; vi 6 3 5 4 6
i\j 0 1 2 3 4 5 6 7 8 9 10
1 0
2 0
3 0
4 0
5 0
What is the optimal value ? Which objects are part of the optimal
solution ?
Exercise 4 : 0-1 Knapsack