Dynamic Programming Via Static Incrementalization 1 Introduction
Total Page:16
File Type:pdf, Size:1020Kb
Dynamic Programming via Static Incrementalization Yanhong A. Liu and Scott D. Stoller Abstract Dynamic programming is an imp ortant algorithm design technique. It is used for solving problems whose solutions involve recursively solving subproblems that share subsubproblems. While a straightforward recursive program solves common subsubproblems rep eatedly and of- ten takes exp onential time, a dynamic programming algorithm solves every subsubproblem just once, saves the result, reuses it when the subsubproblem is encountered again, and takes p oly- nomial time. This pap er describ es a systematic metho d for transforming programs written as straightforward recursions into programs that use dynamic programming. The metho d extends the original program to cache all p ossibly computed values, incrementalizes the extended pro- gram with resp ect to an input increment to use and maintain all cached results, prunes out cached results that are not used in the incremental computation, and uses the resulting in- cremental program to form an optimized new program. Incrementalization statically exploits semantics of b oth control structures and data structures and maintains as invariants equalities characterizing cached results. The principle underlying incrementalization is general for achiev- ing drastic program sp eedups. Compared with previous metho ds that p erform memoization or tabulation, the metho d based on incrementalization is more powerful and systematic. It has b een implemented and applied to numerous problems and succeeded on all of them. 1 Intro duction Dynamic programming is an imp ortant technique for designing ecient algorithms [2, 44 , 13 ]. It is used for problems whose solutions involve recursively solving subproblems that overlap. While a straightforward recursive program solves common subproblems rep eatedly, a dynamic programming algorithm solves every subproblem just once, saves the result in a table, and reuses the result when the subproblem is encountered again. This can reduce the time complexity from exp onential to p olynomial. The technique is generally applicable to all problems whose ecient solutions involve memoizing results of subproblems [4, 5]. Given a straightforward recursion, there are two traditional ways to achieve the e ect of dynamic programming [13 ]: memoization [32 ] and tabulation [5 ]. Memoization uses a mechanism that is separate from the original program to save the result of each function call or reduction [32 , 18 , 21 , 33 , 23 , 41 , 43 , 37 , 17 , 1 ]. The idea is to keep a separate table of solutions to subproblems, mo dify recursive calls to rst lo ok up in the table, and then, if the subproblem has b een computed, use the saved result, otherwise, compute it and save the result in the table. This metho d has two advantages. First, the original recursive program needs virtually no change. The underlying interpretation mechanism takes care of the table lling and lo okup. Second, only values needed by the original program are actually computed, which is optimal in The authors gratefully acknowledge the supp ort of NSF under Grant CCR-9711253. Authors' address: Computer Science Department, Indiana University, Lindley Hall 215, Blo omington, IN 47405. Phone: 812-855-f4373,7979g. Email: fliu,[email protected]. 1 a sense. Memoization has two disadvantages. First, the mechanism for table lling and lo okup has an interpretiveoverhead. Second, no general strategy for table management is ecient for all problems. Tabulation determines what shap e of table is needed to store the values of all p ossibly needed sub computations, intro duces appropriate data structures for the table, and computes the table entries in a b ottom-up fashion so that the solution to a sup erproblem is computed using available solutions to subproblems [5 , 12 , 38 , 37 , 9, 11 , 39 , 40 , 20 , 10 ]. This overcomes b oth disadvantages of memoization. First, table lling and lo okup are compiled into the resulting program so no separate mechanism is needed for the execution. Second, strategies for table lling and lo okup can be sp ecialized to be ecient for particular problems. However, tabulation has two drawbacks. First, it generally requires a thorough understanding of the problem and a complete manual rewrite of the program [13 ]. Second, to statically ensure that all values p ossibly needed are computed and stored, a table that is larger than necessary is often used; it may also include solutions to subproblems not actually needed in the original computation. This pap er presents a p owerful metho d that statically analyzes and transforms straightforward recursive programs to eciently cache and use the results of needed subproblems at appropriate program p oints in appropriate data structures. The metho d has three steps: (1) extend the original program to cache all p ossibly computed values, (2) incrementalize the extended program, with resp ect to an input increment, to use and maintain all cached results, (3) prune out cached results that are not used in the incremental computation, and nally use the resulting incremental program to form an optimized program. The metho d overcomes b oth drawbacks of tabulation. First, it consists of static program analyses and transformations that are general and systematic. Second, it stores only values that are necessary for the optimization; it also shows exactly when and where subproblems not in the original computation are necessarily included. Our metho d is based on static analyses and transformations studied previously by others [49 , 8, 46 , 6 , 34 , 19 , 47 , 39 ] and ourselves [31 , 30 , 29 , 25 , 30 ] and improves them. Yet, all three steps are simple, automatable, and ecient and have been implemented in a prototyp e system, CACHET. The system has b een used to optimize many programs written as straightforward recursions, includ- ing all dynamic programming problems found in [2 , 44 , 13]. Performance measurements con rm drastic asymptotic sp eedups. 2 Formulating the problem Straightforward solutions to many combinatorics and optimization problems can be written as simple recursions [44 , 13 ]. For example, the matrix-chain-multiplication problem [13 , pages 302- 314] computes the minimum number of scalar multiplications needed by any parenthesization in multiplying a chain of n matrices, where matrix i has dimensions p p . This can b e computed i1 i as m(1;n), where m(i; j ) computes the minimum numb er of scalar multiplications for multiplying matrices i through j and can b e de ned as: for i j , 0 if i = j m(i; j )= min fm(i; k )+m(k +1;j)+p p p g otherwise i1 j ik j 1 k The longest-common-subsequence problem [13 , pages 314{320] computes the length c(n; m) of the longest common subsequence of two sequences hx ;x ; :::; x i and hy ;y ; :::; y i, where c(i; j ) can 1 2 n 1 2 m b e de ned as: for i; j 0, ( 0 if i =0 or j =0 c(i 1;j 1)+1 if i 6= 0 and j 6= 0 and x = y c(i; j )= i j max(c(i; j 1);c(i 1;j)) otherwise 2 Both of these examples are literally copied from the textb o ok by Cormen, Leiserson, and Rivest [13 ]. These recursive functions can b e written straightforwardly in the following rst-order, call-by- value functional programming language. A program is a function f de ned by a set of mutually 0 recursive functions of the form f (v ; :::; v ) , e 1 n where an expression e is given by the grammar e ::= v variable j c(e ; :::; e ) constructor application 1 n j p(e ; :::; e ) primitive function application 1 n j f (e ;:::;e ) function application 1 n j if e then e else e conditional expression 1 2 3 j let v = e in e binding expression 1 2 We include arrays as variables and use them for indexed access such as x and p ab ove. For i j convenience, we allow global variables to be implicit parameters to functions; such variables can be identi ed easily for our language even if they are given as explicit parameters. Figure 1 gives programs for the examples ab ove. Invariants ab out an input are not part of a program but are written explicitly to b e used by the transformations. These examples do not use data constructors, but our previous pap ers contain a numb er of examples that use them [31 , 30 , 29 ] and our metho d handles them. m(i; j ) where i j , if i = j then 0 c(i; j ) where i; j 0 else msub(i; j; i) , if i =0 _ j =0 then 0 msub(i; j; k ) where i k j 1 else if x[i]=y [j ] then c(i 1;j 1) + 1 , let s = m(i; k )+m(k +1;j)+p[i 1] p[k ] p[j ] in else max(c(i; j 1);c(i 1;j)) if k +1 = j then s else min(s; msub(i; j; k + 1)) Figure 1: Example programs. These straightforward programs rep eatedly solve common subproblems and take exp onential time. We transform them into dynamic programming algorithms that p erform ecient caching and take p olynomial time. We use an asymptotic cost mo del for measuring time complexity. Assuming that all primitive functions take constant time, we need to consider only values of function applications as candidates for caching. Caching takes extra space, which re ects the well-known trade-o between time and space. Our primary goal is to improve the asymptotic running time of the program. Our secondary goal is to save space bycaching only values useful for achieving the primary goal. Caching requires appropriate data structures. In Step 1, wecache all p ossibly computed results in a recursive tree following the structure of recursive calls.