Notes #15 --- Dynamic Programming
Combinatorial Optimization Problems
We have considered three basic algorithm schemes in this course:
We want to consider optimization problems, in which we want a best solution out of a large set of possible answers to a particular problem instance. In particular, a combinatorial optimization problem is one in which the underlying search space includes all subsets, or all permutations, or a similar combinatorial object. Greedy and divide-and-conquer algorithms handle a few of these problems, generally those in some specialized form. When a problem can be solved by one of these, the solution is generally efficient, since each makes progress toward the eventual solution with each step.
Two other classes of algorithm which can be used for most optimization problems are the exhaustive algorithm scheme, which looks at all the possibilities and chooses the best one, and heuristics, which we will encounter in Artificial Intelligence.
This creates tradeoffs among generality (greedy and divide-and-conquer techniques are applicable only in limited cases), efficiency (exhaustive algorithms for combinatorial optimization problems often have exponential cost), and precision (heuristics are imprecise). We would of course like a general, efficient, and precise technique.
Dynamic programming is very nearly such a technique.
The key notion
More-or-less, all dynamic programming algorithms rely on an encoding of states in an increasing linear way (although there are multidimensional analogues). Those we consider here will have an ordered set of inputs, and the subproblems we solve will handle a subinterval of those inputs. Basically, we can formulate a problem with dynamic programming if:
In a general tree-based dynamic programming algorithm with inputs 1, 2, ..., n, we create solutions for every possible subtree i..j. The general form of the algorithm is more-or-less as follows:
determine the cost of the base cases Cost [ i, i ]
/* don't need to determine structure since it's usually trivial */
for k = 1 to n-1 do
for i = 1 to n-k do
/* determine the cost for i .. i+k */
/* record both the cost and the structure which gave the cost */
/* remembering the root of each tree */
/* is sufficient to reconstruct the tree */
Index_Set = i .. i+k
Cost [ i, i+k ] = min_{ r in Index_Set } { Cost [ i, r] + Cost [ r, i+k ]
+ Merge_Cost ( i, r, i+k ) }
root [ i, i+k ] = the r which resulted in minimum cost
reconstruct the tree using the root table
Notes
The complexity of the dynamic programming algorithm without further optimization is O ( n^3 ) (times the complexity of the individual Merge_Cost operations). In general, we compute O ( n^2 ) best cases, and use O (n) of them in the final solution. Thus we do n times more work than we need. Compare this to the greedy algorithm, which does more-or-less exactly what it needs (but will not always be applicable), and the exhaustive algorithm, which will need to look at more than O ( 2^n ) trees.
Three Examples
List Merge
We wish to use repeated binary merges on a set of sorted lists L1, L2, L3, ..., Ln, each with known size ni, to obtain a single sorted list, with minimal merge cost. It should be clear that the cost of merging two lists L' and L'' is essentially the sum of their lengths. This problem has two variants, and illustrates nicely the difference between a greedy and a dynamic programming algorithm.
(L1,35,0),(L2,60,0),(L5,70,0),(L3,90,0),(L4,140,0)
T1
L1 L2
(L5,70,0),(L3,90,0),(T1,95,95),(L4,140,0)
(T1,95,95),(L4,140,0),(T2,160,160)
(T2,160,160),(T3,235,330)
(T4,395,885)
T4
T2 T3
L5 L3 T1 L4
L1 L2
[Much more to be added.]