Bu I 7 - Dynamic Programming Algorithms
Bu I 7 - Dynamic Programming Algorithms
Bu I 7 - Dynamic Programming Algorithms
6 octobre 2020
Outline
Problems with divide-and-conquer
Introduction to dynamic programming
The making change problem
Optimal substructure
0-1 Knapsack Problem
Matrix-chain multiplication
Longest common subsequence
Floyd’s algorithm
Problems that fail the optimal substructure test
Conclusion
Final exercises
Problems with divide-and-conquer
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”.
#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;
}
Making change : DP approach
Amount 0 1 2 3 4 5 6 7 8
d1 = 1
d2 = 4
d3 = 6
The initialization of the table is obtained from the D&C base case :
if (j == 0) then return 0
Amount 0 1 2 3 4 5 6 7 8
d1 = 1 0
d2 = 4 0
d3 = 6 0
Making change : DP approach
Amount 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
For example, the content of entry t[1, 4] = t[1, 3] + 1 means that the
minimum number of coins to return 4 units using only denomination 1
is the minimum number of coins to return 3 units + 1 = 4 coins.
Making change : DP approach
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
d3 = 6 0 1 2 3 1 2
For all the other entries of the table we write the code of the DP
algorithm using the recursive function
Designing DP from the D&C recursive calls
Here we look at the recursive call of the D&C to design the table
look-up of the DP algorithm :
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(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] ;
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
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
We can use the information in the table to get the list of coins that
should be returned :
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
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
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.
#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 || W == 0) return 0;
if (wt[n - 1] > W) return K(W, wt, val, n - 1);
else return max( val[n - 1] + K(W - wt[n - 1], 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));
return 0;
}
Divide & Conquer 0-1 Knapsack
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
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
the recursive function for 0-1 knapsack :
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 ] ;
0-1 Knapsack Example
i 1 2 3 4 5 6
wi 3 2 6 1 7 4
vi 7 10 2 3 2 6
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
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
Two (of the 5) ways to compute this product are (A1 (A2 (A3 A4 ))) and
((A1 A2 )(A3 A4 )).
MCM Example
(A1 ( A2 ( A3 A4 )) ) ( (A1 A2 ) ( A3 A4 ))
100x3 3x20 20x50 50x150 100x3 3x20 20x50 50x150
A1 A2 A3 A4 A1 A2 A3 A4
A1 A2 B A1 A2 B
The cost of the optimal solution is the cost of each of the two
subsolutions plus the cost of multiplying the final two matrices.
An optimal subsolution will split the subproduct into two halves, each
half of which is optimal as well.
Solving MCM
int M(i, j)
if i == j then return (0) ;
else
return mini≤k<j (M(i, k) + M(k + 1, j) + di−1 dk dj ) ;
M[2,2] M[3,4] M[2,3] M[4,4] M[1,1] M[2,2] M[3,3] M[4,4] M[1,1] M[2,3] M[1,2] M[3,3]
M[1,4]
=Already Computed
M[2,2] M[3,4] M[2,3] M[4,4] M[1,1] M[2,2] M[3,3] M[4,4] M[1,1] M[2,3] M[1,2] M[3,3]
I Notice that many of the calls are repeated (all the shaded boxes).
I The divide-and-conquer algorithm has the following recurrence
n−1
X
T (n) = (T (k) + T (n − k)) + cn
k=1
If n > 0,
n−1
X
T (n) = 2 T (k) + cn
k=1
Therefore Pn−1
T (n) − T (n − 1) = (2 k=1 T (k) + cn)
Pn−2
− (2 k=1 T (k) + c(n − 1))
= 2T (n − 1) + c
That is
T (n) = 3T (n − 1) + c
Analysis of the recurrence for MCM
T (n) = 3T (n − 1) + c
= 3(3T (n − 2) + c) + c
= 9T (n − 2) + 3c + c
= 9(3T (n − 3) + c) + 3c + c
= 27T (n − 3) + 9c + 3c + c
Pk−1
= 3k T (n − k) + c l=0 3l
Pn−1
= 3n T (0) + c l=0 3l
n
= c3n + c 3 2−1
= (c + c2 )3n − c2
T (n) ∈ Θ(3n ).
1 2 3 4 5 1 2 3 4 5
0 120 1 0 120 264 1
0 360 2 0 360 1320 2
⇒
0 720 3 0 720 1140 3
0 1680 4 0 1680 4
0 5 0 5
MCM Example (continued again)
1 2 3 4 5 1 2 3 4 5
0 120 264 1 0 120 264 1080 1
0 360 1320 2 0 360 1320 1350 2
⇒
0 720 1140 3 0 720 1140 3
0 1680 4 0 1680 4
0 5 0 5
MCM Example (continued again)
1 2 3 4 5 1 2 3 4 5
0 120 264 1080 1 0 120 264 1080 1344 1
0 360 1320 1350 2 0 360 1320 1350 2
⇒
0 720 1140 3 0 720 1140 3
0 1680 4 0 1680 4
0 5 0 5
MCM Example : Optimal Cost and Solution
Each time we find the optimal value for M[i, j], we also store the value
of k that we used.
1 2 3 4 5
0 120/1 264/2 1080/2 1344/2 1
0 360/2 1320/2 1350/2 2
0 720/3 1140/4 3
0 1680/4 4
0 5
The optimal solution for the second half comes from entry M[3, 5].
This
Pn algorithm has 3 nested loops. The summation is
Pn−1 Pi+s
s=1 i=1 k=i 1. The complexity of dynamic programming MCM is
3
Θ(n ).
Exercises 6 and 7 on matrix-chain multiplications
Let lcs(X [1..m], Y [1..n]) be the length of the LCS of X and Y . The
length of the LCS is computed recursively as follow :
Building a partial call tree for sequences ”AXYT” and ”AYZX”, it can
be verified that the recursive algorithm solves the same subproblems
several times. Soon you will observe that lcs(”AXY ”, ”AYZ ”) is being
solved twice.
Top Down DP algorithm for LCS
LCS-Length(X,Y,m,n)
let b[1..m, 1..n] and c[0..m, 0..n]
for i = 1 to m c[i, 0] = 0
for j = 0 to n c[0, j] = 0
for i = 1 to m
for j = 1 to n
if X [i] == Y [j]
c[i, j] = c[i − 1, j − 1] + 1
b[i, j] =”-”
else if c[i − 1, j] ≥ c[i, j − 1]
c[i, j] = c[i − 1, j]
b[i, j] =”↑”
else c[i, j] = c[i, j − 1]
b[i, j] =”←”
return c and b
LCS-Length(X,Y,m,n)
let b[1..m, 1..n] and c[0..m, 0..n]
for i = 1 to m c[i, 0] = 0
for j = 0 to n c[0, j] = 0
for i = 1 to m
for j = 1 to n
if X [i] == Y [j]
c[i, j] = c[i − 1, j − 1] + 1
b[i, j] =”-”
else if c[i − 1, j] ≥ c[i, j − 1]
c[i, j] = c[i − 1, j] X = [ABCBDAB]
b[i, j] =”↑”
Y = [BDCABA]
else c[i, j] = c[i, j − 1]
b[i, j] =”←” m = 7; n = 6
return c and b LCS is [BCBA]
Printing the LCS
Print-LCS(b,X,i,j)
if i == 0 or j == 0 return
if b[i, j] == ”-”
Print-LCS(b,X,i-1,j-1)
print xi
else if b[i, j] == ”↑”
Print-LCS(b,X,i-1,j)
else if b[i, j] == ”←”
Print-LCS(b,X,i,j-1)
Problem definition :
I Let G = {V , E } be a connected “directed” graph where V is the
set of nodes (|V | = n) and E is the set of edges.
I Each edge has an associated nonnegative length
The All-Pairs Shortest-Path Problem
We have a distance matrix L[i, j] that gives the length of each edge :
I L[i, i] = 0, L[i, j] ≥ 0 if i 6= j,
I L[i, j] = ∞ if the edge (i, j) does not exist.
The goal is to find the shortest path between each pair of nodes i and
j using {1, 2, . . . , n}
Shortest path D&C
function S − Path(i, j, k)
if (k == 0) then return L[i, j] ; /* Base case */
else
return min(S − Path(i, j, k − 1),
S − Path(i, k, k − 1) + S − Path(k, j, k − 1))
The initial call is S − Path(i, j, n). This function is run for each pair of
node i and j
A matrix D that gives the length of the shortest path between each
pair of nodes.
After each iteration k, D contains the length of the shortest paths that
only use nodes in {1, 2, . . . , k} as intermediate nodes.
Floyd Algorithm
At iteration k, the algo checks each pair of nodes (i, j) whether or not
there exists a path from i to j passing through node k that is better
than the present optimal path passing only through nodes in
{1, 2, . . . , k − 1}.
If Dk represents the matrix D after the k-th iteration (so D0 = L), the
necessary check can be implemented by
For k = 1, compute the shortest path between each pair of nodes (i, j)
when the path is allowed to pass through node 1.
Execution of Floyd’s algorithm
Algorithm Floyd(L[n, n])
D =L
for (k = 1; k ≤ n; k + +)
for (i = 1; i ≤ n; i + +)
for (j = 1; j ≤ n; j + +)
D[i, j] = min(D[i, j], D[i, k] + D[k, j])
return D
For k = 2, compute the shortest path between each pair of nodes (i, j)
when the path is allowed to pass through nodes 1 and 2.
Execution of Floyd’s algorithm
Algorithm Floyd(L[n, n])
D =L
for (k = 1; k ≤ n; k + +)
for (i = 1; i ≤ n; i + +)
for (j = 1; j ≤ n; j + +)
D[i, j] = min(D[i, j], D[i, k] + D[k, j])
return D
For k = 3, compute the shortest path between each pair of nodes (i, j)
when the path is allowed to pass through nodes {1, 2, 3}.
Execution of Floyd’s algorithm
Algorithm Floyd(L[n, n])
D =L
for (k = 1; k ≤ n; k + +)
for (i = 1; i ≤ n; i + +)
for (j = 1; j ≤ n; j + +)
D[i, j] = min(D[i, j], D[i, k] + D[k, j])
return D
0 0 0 0
0 0 0 0
0 1 0 0
0 1 0 0
Computing the shortest paths : k = 2
0 0 2 2
0 0 0 0
0 1 0 0
0 1 0 0
Computing the shortest paths : k = 3
0 0 2 2
3 0 0 0
0 1 0 0
0 1 0 0
Computing the shortest paths : k = 4
0 0 4 2
4 0 4 0
0 1 0 0
0 1 0 0
Computing the shortest paths
0 0 4 2
4 0 4 0
P=
0
1 0 0
0 1 0 0
10. Compute the all pairs of shortest paths for the following oriented
graph.
0 5 10 3
∞ 0 1 4
L=
∞
∞ 0 ∞
∞ ∞ 2 0
11. Compute the all pairs of shortest paths for the following oriented
graph.
0 3 8 ∞ 4
∞ 0 ∞ 1 7
∞
L= 4 0 ∞ ∞
2 ∞ 5 0 ∞
∞ ∞ ∞ 6 0
Cost of dynamic programs
Calculate how many table or list entries you fill in (sometimes you
don’t use the entire table, just all entries under the diagonal or
something like that).
Then add the cost of retrieving the answer from the table.
Example : problem fails the optimal substructure test
I a time interval [si , fi ] where si is the start time when the activity should
begin and fi is the moment when the activity should finish
1 5
2 3
3 5
4 4
5 2
6 5
7 3
I If the optimal schedule achieving the benefit Bi does not include request
i, then Bi = Bi−1
HST (L, i)
if (i == 0) Bi = 0 ;
else
Bi = max{HST (L, i − 1), HST (L, pred(i)) + bi }