Chapter 15 - Dynamic Programming |
Document last modified: |
Overview
Dynamic programming (i.e. planning) is a technique for solving problems that have overlapping subproblems.
One goal is to reduce the complexity of a problem; we will see that this can often be accomplished by remembering and recalling subproblem solutions rather than recomputing the solution multiple times.
Not a specific algorithm, but a technique (like divide-and-conquer).
Used in optimization problems.
- Find a solution with the optimal value.
- Minimization or maximization.
Example - Recursive Fibonacci function
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2 for n³2Analysis
- F5 at right is a full binary tree down to a depth of 5/2=2.
- Generally for Fn, n/2, the rightmost path being the shortest and has more nodes at lower depths.
- The running time is at least W(2n/2) or exponential. Recall that a complete binary tree of height h has 2h+1−1 nodes with 2h at the bottom.
Analysis T(n) = Q( fn )
T(n) = T(n-1) + T(n-2) + Q(1)
Assume T(n) ≤ aFn - b a > 1 and b > 0
T(n) = T(n-1) + T(n-2) + Q(1) ≤ aFn-1 - b + aFn-2 - b + Q(1) ≤ a(Fn-1 + Fn-2) - 2b + Q(1) = aFn - b -(b - Q(1)) ≤ aFn - b for b ³ Q(1) T(n) = Q( fn ) where f = 1.61803..., the golden ratio, and Fn = fn (see equation 3.25)
Example - Using subproblem solutions to compute the problem solution.
The problem with the recursive solution is the recomputing of solutions to subproblems; F1 is computed 5 times.
The following algorithm trades space for time, requiring 6 memory spaces for a Q(n) time and space solution.
f[0] ¬ 0 f[1] ¬ 1
for i¬2 to n do
f[ i ] ¬ f[i - 1] + f[i - 2]
i 0 1 2 3 4 5 f 0 1 1 2 3 5 Developing solutions - Four-step method
- Characterize the structure of an optimal solution.
Example: define the Fibonacci sequence
- Recursively define the value of an optimal solution
Example: F(n) = F(n-1) + F(n-2)
- Compute the value of an optimal solution in a bottom-up fashion.
Example: compute F(0) and F(1)
- Construct an optimal solution from computed information.
Example: use F(0) and F(1) to compute F(2), then use F(1) and F(2) to compute F(3), ...
Memo-ization
• “Store, don’t recompute.”
• Make a table indexed by subproblem.
• When solving a subproblem:
• Lookup in table.
• If answer is there, use it.
• Else, compute answer, then store it.
• In dynamic programming, go one step further. Determine in what order want to access the table, and fill it in that way.Generally the recursive procedure A can be converted into dynamic programmed ADP by the following technique.
Assume P is the current problem:
- Before any recursive call to solve s, a subproblem of P, check solution dictionary D for s.
- Lookup s in dictionary D
- If s is not in dictionary, make recursive call to compute s
- Use s to compute solution to P.
- Insert P and solution into dictionary D.
- return solution
Example
Recursive Fibonacci fib( n ) if n<2 then return n
return fib( n-1 ) + fib( n-2)
Memo-izated Fibonacci fib( n ) Dictionary solutions ¬ new( n ) -- Create dictionary for n solutions
insert( solutions, 0, 0 ) -- insert key=0 and solution 0
insert( solutions, 1, 1 ) -- insert key=1 and solution 1
return fibDP( n ) -- Return nth solution
fibDP( k )
if f1 ¬ lookup( k-1 ) = NIL then -- if exists use stored k-1 result
f1 ¬ fibDP( k-1 ) -- otherwise computeif f2 ¬ lookup( k-2 ) = NIL then -- if exists use stored k-2 result
f2 ¬ fibDP( k-2 ) -- otherwise computef ¬ f1 + f2
insert( solutions, k, f ) -- insert key=k and solution f
return f
Question 15.1
- Draw the recursion tree for fibDP( 6 ) and the solutions dictionary.
- How many calls?
- What is the O running time in terms of the calls?
Discrete Knapsack problem - an optimization
Suppose you've gotten lucky and won a shopping spree in which you can fill your knapsack with any items in the store.
With a knapsack of capacity W, you would like to put in the maximum value of items.
Brute force/exhaustive search
Solution: One way is to compute all possible solutions while keeping track of the maximum.
Analysis: If only one of each item, there are 2n subsets of n items (power set) to consider.
For example: 3 items, A, B, C would be: {Æ}, {A}, {B}, {C}, {A,B}, {A,C}, {B,C}, {A,B,C}.
Complexity is W(2n) or exponential, 23=8.
Example:
- Suppose there one of each item.
- Knapsack holds size W=10 capacity.
Items
1 2 3 item A B C size 7 3 4 value $42 $12 $40
Exhaustive search - {A,B} optimal solution, $54
Subset Total size Total value Æ 0 $0 {A} 7 $42 {B} 3 $12 {C} 4 $40 {A,B} 10 $54 {A,C} 11 not feasible {B,C} 7 $52 {A,B,C} 14 not feasible
Question 15.2.1 Suppose our knapsack held W=13? What is the optimal solution?
Example:
- Now suppose there are as many of each item as you want.
At right, the knapsack has a capacity of W=17 and the items are:
1 2 3 4 5 item A B C D E size 3 4 7 8 9 value 4 5 10 11 13 3 A's and 2 B's is a value 22 and size 17. 4+4+4+5+5=22.
One item D and E is a value 24 and size 17, a maximum.
11+13=24.
How can we determine that?
Example:
For capacity 8 the recursion tree below computes the maximum value 11.
-3/9 indicates size=3 subtracted from capacity of 8, and value=9 returned from child.
The maximum value=11 is returned by the root's D child.
Non-feasible solutions, not all shown, return 0 (e.g. triangle).
| int knapsack( capacity ) | ||
| 1 | max ¬ 0 | |
| 2 | for i ¬ 1 to N | -- N number of items |
| 3 | if space ¬ capacity - size[ i ] ≥ 0 then | -- Room for item i |
| 4 | if tempmax ¬ knapsack( space ) + value[ i ] > max then | |
| 5 | max ¬ tempmax | |
| 6 | return max |
|
|
Solutions to knapsack( 17 )
The recursion tree above shows capacity for each call of knapsack.
- □ are leafs where no item of that capacity exists.
- All 0 leafs are solutions that fill the knapsack but non-zero solutions could be optimal.
- {A,A,B,B,A} is path <3,3,4,4,3>, filling the sack.
- There are some omissions in the diagram (e.g. at far left, capacity 8 results from putting a size 9, E, in sack; there should be a leaf of 0 after putting D, size 8, in sack).
- Line 3 excludes non-feasible solutions reducing the recursion tree size.
- It has 129 calls (determined empirically).
Analysis: Allowing multiples of any of n items to fill the knapsack to capacity W.
Each call executes for i ¬ 1 to N, Q(n).
Each call may generate W recursive calls on smaller capacities, O(W).
Complexity is O(nW).
Question 15.3
- What value is returned from path <3,3,4,4,3>? Is that optimal?
- Generate the first few (7 or 8) recursive calls to knapsack( 17 ).
- Is this a greedy algorithm? Does it always choose the best choice to solve for a solution or do extra work?
1 2 3 4 5 item A B C D E size 3 4 7 8 9 value 4 5 10 11 13
int knapsack( capacity ) 1 max ¬ 0 2 for i ¬ 1 to N -- N number of items 3 if space ¬ capacity - size[ i ] ≥ 0 then -- Room for item i 4 if tempmax ¬ knapsack( space ) + value[ i ] > max then 5 max ¬ tempmax 6 return max
Optimal substructure necessary for dynamic programming
An optimal solution to a problem contains within it an optimal solution to subproblems.
Example: We saw that Fibonacci( 5 ) = Fibonacci( 4 ) + Fibonacci( 3 ).
Solution to problem Fibonacci( 5 ) contains the solution to subproblem Fibonacci( 4 ) and Fibonacci( 3 ).
Example: The optimal solution to a capacity 17 sack is D and E of size 8 and 9 respectively.
Solution to problem capacity 17 sack contains the optimal solution to subproblem capacity 8 and 9.
Key ideas: Use optimal substructure to construct optimal solution to problem from optimal solutions to subproblems.
Use memoization to maintain the computed maximum value for a given capacity.
When recursing, use the recorded maximum value when available.
When iterating, update the recorded maximum if a larger value for a capacity is computed.
Example:
For capacity 8 the recursion tree below computes the maximum value 11.
knapsack( 1 ) and knapsack( 0 ) calls are not repeated.
knapsack( 8 )
-3 / 9
-4 | 10
-7 | 10 -8 \
11 -9 \
0
knapsack(5)
knapsack(4) knapsack( 1 )
knapsack( 0 ) Æ
-3 / 4 -4 \ 5
-3 / 4 -4 \ 5
knapsack(2) knapsack(1) knapsack(1)
knapsack(0)
| 0
| 0
| 0
Æ
Æ
Æ
|
Solutions database for knapsack( 8 )
|
| int knapsack( capacity ) | ||
| 1 | if max ¬ lookup( capacity ) = NIL then | -- if exists use max value for capacity |
| 2 | max ¬ 0 | -- otherwise compute value for capacity |
| 3 | for i ¬ 1 to N | -- N is number of items |
| 4 | if space ¬ capacity - size[ i ] ≥ 0 then | -- Room for item i |
| 5 | if tempmax ¬ knapsack( space ) + value[ i ] > max then | |
| 6 | max ¬ tempmax | |
| 7 | insert( solutions, capacity, max ) | -- Store max keyed on capacity |
| 8 | return max |
|
The complete recursion tree has 45 calls (determined empirically). ![]() Solutions database for knapsack( 17 )
|
Question 15.4
- Does the algorithm exhibit greedy behavior?
- Why doesn't 12, 15, or 16 occur in the solutions database?
- Is the above recursion tree complete for the dynamically programmed version? Compare <17, 9, 6, 3> and <17, 13, 6>.
- Why does the dynamically programmed recursion tree above differ from the brute force tree below?
- What would be returned from <17, 14, 11, 7>?
Assembly-line scheduling
A slightly more complicated example from Cormen. Actually solvable by a graph algorithm but a good, detailed introduction to dynamic programming.
Automobile factory with two assembly lines with n stations.
• Sline,station or S1,1, . . . , S1,n and S2,1, . . . , S2,n.
• Corresponding stations S1,j and S2,j perform the same function but can take different amounts of time a1,j and a2,j
• Entry times to each line, e1 and e2.
• Exit times from each line, x1 and x2.
• After going through a station, can either:
- stay on same line; no cost, or
- transfer to other line; cost after Si,j is ti,j. ( j = 1, . . . , n−1. No ti,n , because the assembly line is done after Si,n.)
Problem: Given all these costs (time = cost), what stations should be chosen from line 1 and from line 2 for fastest way through factory?
Try all possibilities?
• Each candidate is fully specified by which stations from line 1 are included. Looking for a subset of line 1 stations.
• Each line has n stations.
• 2n subsets (2 lines with 4 stations, 28 = 256 different ways, of which 16 are viable solutions, 256 ways includes path using no station and illegal transfers such as: S1,2 ® S1,4).
• Impractical when n is large.Structure of an optimal solution
Think about fastest way from entry through S1,j.
• If j = 1: just determine how long it takes to get through S1,1.
• If j ≥ 2, have two choices of how to get to S1,j :
- Through S1,j-1, then directly to S1,j .
- Through S2,j-1, then transfer over to S1,j .
Suppose fastest way is through S1,j-1.
Key observation: We must have taken a fastest way from entry through S1,j-1 in this solution. If there were a faster way through S1,j-1, we would use it instead to come up with a faster way through S1,j.
Suppose instead fastest way is through S2,j-1. Again, we must have taken a fastest way through S2,j-1. Otherwise use some faster way through S2,j-1 to give a faster way through S1,j
Optimal substructure:
Generally: An optimal solution to a problem (fastest way through S1,j ) contains within it an optimal solution to subproblems (fastest way through S1,j-1 or S2,j-1).
Use optimal substructure to construct optimal solution to problem from optimal solutions to subproblems.
Fastest way through S1,j is either
- fastest way through S1,j-1 then directly through S1,j, or
- fastest way through S2,j-1, transfer from line 2 to line 1, then through S1,j .
Symmetrically: Fastest way through S2,j is either
- fastest way through S2,j-1 then directly through S2,j, or
- fastest way through S1,j-1, transfer from line 1 to line 2, then through S2,j.
Therefore, to solve problems of finding a fastest way through S1,j and S2,j, solve subproblems of finding a fastest way through S1,j-1 and S2,j-1.
Recursive solution
Let fi [ j ] = fastest time to get through Si,j , i = 1,2 and j = 1, . . . , n.
e1 and e2 entry time
x1 and x2 exit time
Transfer time is ti,j
Station time is ai,j
Goal: fastest time to get all the way through = f*.
f* ¬ min( f1[n] + x1, f2[n] + x2) -- Minimum time to exit either line 1 or 2
f1[1] ¬ e1 + a1,1 f2[1] ¬ e2 + a2,1
for j ¬ 2, . . . , n:
f1[ j ] ¬ min( f1[ j−1] + a1,j , f2[ j−1] + t2,j−1 + a1,j )
f2[ j ] ¬ min( f2[ j−1] + a2,j , f1[ j−1] + t1,j−1 + a2,j )
f1[ j−1] + a1,j gives the value for staying on line 1 in station j.
f2[ j−1] + t2,j−1 + a1,j gives the value for transferring to line 1 in station j.
f1[ j ] gives the value of an optimal solution to station j of line 1.
f2[ j ] gives the value of an optimal solution to station j of line 2.
Constructing an optimal solution
• li [ j ] = line # (1 or 2) whose station j−1 is used in fastest way through Si,j
• In other words Sli [ j ], j−1 precedes Si,j
• Defined for i = 1,2 and j = 2, . . . , n.
• l* = line # whose station n is used; the exit line.Example - f* and l* for assembly line
Note: f2[ 2 ] ¬ min( f2[1] + a2,2 , f1[1] + t1,1 + a2,2 ) = min( 12+5 , 9+2+5 ) = min( 17 , 16 ) =
Question 15.5
To determine the solution, trace backward through the optimal path given by li values starting with i=l*.
Compute an optimal solution
Recursive algorithm based on above recurrences.
• Let ri ( j ) = # of references made to fi [ j ].
• r1(n) = r2(n) = 1.
• r1( j ) = r2( j ) = r1( j + 1) + r2( j + 1) for j = 1, . . . , n − 1.Claim: ri ( j ) = 2n−j .
Proof: Induction on j , down from n.
Basis: j = n. 2n−j = 20 = 1 = ri (n).
Inductive step: Assume ri ( j + 1) = 2n−( j+1).
Then ri( j ) = ri( j + 1) + r2( j + 1)
= 2n−( j+1) + 2n−( j+1)
= 2n−( j+1)+1
= 2n−j .Therefore, f1[1] alone is referenced 2n−1 times! So top down isn’t a good way to compute f1[ j ].
Observation: fi [ j ] depends only on f1[ j − 1] and f2[ j − 1] (for j ≥ 2). So compute in order of increasing j .
Example - f* and l* for assembly line
Output: line 1, station 5
line 1, station 4
line 2, station 3
line 1, station 2
line 1, station 1
Elements of dynamic programming
Optimal substructure
• Show that a solution to a problem consists of making a choice, which leaves one or subproblems to solve.
• Suppose that you are given this last choice that leads to an optimal solution. People often have trouble understanding the relationship between optimal substructure and determining which choice is made in an optimal solution. One way that helps understanding optimal substructure is to imagine that “God” tells you what was the last choice made in an optimal solution.
• Given this choice, determine which subproblems arise and how to characterize the resulting space of subproblems.
• Show that the solutions to the subproblems used within the optimal solution must themselves be optimal. Usually use cut-and-paste.
• Suppose that one of the subproblem solutions is not optimal.
• Cut it out.
• Paste in an optimal solution.
• Get a better solution to the original problem. Contradicts optimality of problem solution. That was optimal substructure.
Need to ensure that you consider a wide enough range of choices and subproblems that you get them all.
Try all the choices, solve all the subproblems resulting from each choice, and pick the choice whose solution, along with subproblem solutions, is best.
How to characterize the space of subproblems?
• Keep the space as simple as possible.
• Expand it as necessary.Assembly-line scheduling
• Space of subproblems was fastest way from factory entry through stations S1, j and S2, j .
• No need to try a more general space of subproblems.
Dynamic programming uses optimal substructure bottom up.
• First find optimal solutions to subproblems.
• Then choose which to use in optimal solution to the problem.When we look at greedy algorithms, we’ll see that they work top down: first make a choice that looks best, then solve the resulting subproblem.
Don’t be fooled into thinking optimal substructure applies to all optimization problems. It doesn’t.
Here are two problems that look similar. In both, we’re given an unweighted, directed graph G = (V, E).
• V is a set of vertices.
• E is a set of edges.And we ask about finding a path (sequence of connected edges) from vertex u to vertex v.
• Shortest path: find path uv with fewest edges. Must be simple (no cycles), since removing a cycle from a path gives a path with fewer edges.
• Longest simple path: find simple path uv with most edges. If didn’t require simple, could repeatedly traverse a cycle to make an arbitrarily long path.
Shortest path has optimal substructure.
• Suppose p is shortest path uv.
• Let w be any vertex on p.
• Let p1 be the portion of p, uw.
• Then p1 is a shortest path uw.
Proof Suppose there exists a shorter path p'1 , u
w. Cut out p1, replace it with p'1 , get path
with fewer edges than p.
Therefore, can find shortest path u
v by considering all intermediate vertices w, then finding optimal sub-problem solutions to shortest paths u
w and w
v.
Same argument applies to p2.
Does longest path have optimal substructure?
- It seems like it should.
- It does not.
Consider q ® r ® t = longest path q
t. Are its subpaths longest paths? No!
• Subpath qr is q ® r.
• Longest simple path qr is q ® s ® t ® r.
• Subpath rt is r ® t.
• Longest simple path rt is r ® q ® s ® t.
Not only isn’t there optimal substructure, but cannot even assemble a legal solution from solutions to subproblems.
Combine longest simple paths:
q ® s ® t ® r ® q ® s ® t
Not a simple path!
In fact, this problem is NP-complete (so it probably has no optimal substructure to find.)
What’s the big difference between shortest path and longest path?
• Shortest path has independent subproblems.
• Solution to one subproblem does not affect solution to another subproblem of the same problem.
• Longest simple path: subproblems are not independent.
• Consider subproblems of longest simple paths qr and r
t.
• Longest simple path qr uses s and t.
• Cannot use s and t to solve longest simple path rt, since if we do, the path isn’t simple.
• But we have to use t to find longest simple path rt!
• Using resources (vertices) to solve one subproblem renders them unavailable to solve the other subproblem. [For shortest paths, if we look at a shortest path, no vertex other than w can appear in p1 and p2. Otherwise, we have a cycle.]
Assembly line independent subproblem example:
1 subproblem Þ automatically independent.
Overlapping subproblems
Occur when a recursive algorithm revisits the same problem over and over: Fibonacci.
Good divide-and-conquer algorithms usually generate a brand new problem at each stage of recursion.
Example: merge sort
from Cormen