Dynamic programming

This set of exercises accompanies Lecture on DynamicProgramming of the course Algorithm Design and Analysis.

Summary

Dynamic programming represents a technique for designing (optimizing) algorithms. It addresses problems that can be decomposed in subproblems, but these subproblems overlap. Instead of solving the same subproblems repeatedly, applying dynamic programming techniques helps to solve each subproblem just once.

Dynamic programming as an algorithm design method comprises several optimization levels:

Examples presented in Class

Binomial coefficients, Integer Knapsack and LCS are presented in Lecture.

Example with complete code: Binomial coefficients

The binomial coefficient C(n, k) is the number of ways of choosing a subset of k elements from a set of n elements.

By its definition, C(n,k)=n! / ((n-k)!*k!)

This definition formula is not used for computation because even for small values of n, the values of n factorial n! get really large.

Instead, C(n,k) can be computed by following formula:

C(n,k)=C(n-1, k-1)+C(n-1, k)

There are several implementations possible for a binomial coefficient solver. The binomial coefficient solver interface implemented by all solvers is IBinomialCoef.java. In this way, the main program can use and experiment with different implementations of the binomial coefficient solver. Such a program is given in ProgramBinomialCoef.java

The implementations of IBinomialCoeff are the following:

  1. An inefficient recursive solution is given in BinomialCoefRec.java. When trying to run the program ProgramBinomialCoef, notice that for n>35 this recursive version has a very long running time, you may remove it from the program.
  2. A recursive solution based on memoization avoid computing the same C(x,y), by using a table to store the results is given in BinomialCoefMemoization.java
  3. An iterative dynamic programming solution is possible when managing to figure out the order in which the table has to be filled out. BinomialCoefDynProg.java
  4. A memory efficient dynamic programming version is possible because we notice that every iteration step works with only 2 rows of the table. The amount of memory required by the program is reduced by using 2 buffers for these 2 rows only and reusing the buffers. BinomialCoefDynProgMemEff.java
  5. You could optimize even more the memory usage allocating the rows of size k intead of size n.

Download code: you may download code as a single archive of source code binomial_src.zip .


Implementation exercises

The Integer Exact Knapsack

See Lecture, slides 24-35

See also in [Manber - 5.10]

Given an integer K and n items of different sizes such that the i-th item has an integer size weight[i], determine if there is a subset of the items whose weights sum to exactly K, or determine that no such subset exist.

Example1: Consider n=8, weights={8,4,1,2,3,1,4,1} and K=16. This problem has a solution (a subset summing the total weight K=16 is {8,4,1,3}).

Example2: Consider n=3, weights={5,6,8} and K=7. This problem has no solution.

The Integer Exact Knapsack problem has 2 versions:

You are given the inefficient recursive implementation for KnapsackSimple_Recursive.java and KnapsackComplete_Recursive.java.

You are also given two test programs, one using a IKnapsackSimple solver respectively a IKnapsackComplete solver. These two programs are ProgramKnapsackSimple.java respectively ProgramKnapsackComplete.java. The test programs run the integer exact knapsack algorithm on 3 sets of randomly generated weights: the first one, in demo mode, for a short set of n=8, the second one for a medium set of n=1000 and the third for a long set of n=10000. While the simple recursive solutions work well for the short set, they will get stack overflow errors for the long set. A dynamic programming solution using a big table will most likely get out of memory errors for very long sets.

Optimize the implementations of the integer exact knapsack solvers such that they can handle long sets of weights. There are following possibilities:

For the Simple Knapsack:

For the Complete Knapsack:

Download code: you may download the six files discussed above (IKnapsackSimple, KnapsackSimple_Recursive, ProgramKnapsackSimple, IKnapsackComplete, KnapsackComplete_Recursive, ProgramKnapsackComplete) as a single archive of source code ks_src.zip .

Longest common subsequence

See Lecture, slides 36-48

See also [CLRS - chap 15.4]

Given 2 sequences, X ={x1, ..., xm} and Y ={y1, ..., yn}. Find a subsequence common to both whose length is longest. A subsequence does not have to be formed by consecutive elements, but the elements have to be in order.

Example: The Length of the LCS of strings HORSEBACK and SNOWFLAKE is 3. Two such LCS are OAK or SAK.

The LCS problem has 2 versions:

You are given the inefficient recursive implementations for LCS_Simple_Recursive.java and LCS_Complete_Recursive.java.

You are also given two test programs, one using a ILCS_Simple solver respectively a ILCS_Complete solver. These two programs are ProgramLCS_Simple.java respectively ProgramLCS_Complete.java. The test programs run the LCS algorithm on 3 pairs of strings: a pair of short strings (about 10 characters), a pair of medium strings (about 20 characters) and a pair of long strings (some hundreds characters). While the simple recursive solutions work well for the short strings, you have to wait a little for getting the result in the case of the medium strings, and most probably you will run out of time waiting for the result of the long strings; and you haven't even tried some very long strings as of thousands of characters.

Optimize the implementations of the LCS solvers such that they can handle long strings. There are following possibilities:

For the Simple LCS:

For the Complete LCS:

Download code: you may download the six files discussed above (ILCS_Simple, LCS_Simple_Recursive, ProgramLCS_Simple, ILCS_Complete, LCS_Complete_Recursive, ProgramLCS_Complete) as a single archive of source code LCS_src.zip .

Additional Exercises

Rod Cutting

See [CLRS - chapter 15.1]

Given a rod of length n inches and an array of prices that includes prices of selling all pieces of size smaller than n. Determine the maximum profit obtainable by cutting up the rod and selling the pieces.

Example: if length of rod is n=4 and prices={1, 5, 8, 9, 10, 17, 17, 20, 24, 30} the maximum profit is 10 (can be obtained cutting 2 pieces of length 2).

Example: if length of rod is n=5 and prices={1, 5, 8, 9, 10, 17, 17, 20, 24, 30} the maximum profit is 13 (can be obtained cutting a piece of length 2 and a piece of length 3).

There is no correlation between lebgths of pieces and their price (the values in array prices are not necessary in increasing order).

Another example: if the length of the rod is n=5 and prices={5, 7, 2, 1, 1, 2, 2, 2, 2, 2} the max profit is 25 (cut 5 pieces of length 1 each).

The problem has two requirements versions:

The Simple result version requires to find out only the value of the maximum profit.

The Complete result version requires to find also the sequence of cuts that lead to the maximum profit.

Here you have implementations as recursive algorithms:

Simple result: RodCuttingSimple.java

Complete result: RodCuttingComplete

Apply techniques of Dynamic Programming in order to reduce the time complexity:

The Minimum Edit Distance

Given two strings X and Y of size m and n, and a set of operations replace, insert and delete. Find the minimum number of operations required to convert one string into another.

Example: First string is: INTENTION Second string is: EXECUTION

In order to transform string X into string Y, the minimum number of operations is 5 operations:
Replace I by E
Replace N by X
Delete T
Replace N by C
Insert U

The problem has two requirements versions:

The Simple result version requires to find out only the number of operations.

The Complete result version requires to find also the sequence of operations.

Here you have implementations as recursive algorithms:

Simple result: MinimumEditDistance.java

Complete result: MinEditDistanceOperations.java

Apply techniques of Dynamic Programming in order to reduce their time complexity:

Count Structurally Unique Binary Search Trees

Given a natural number N, count how many structurally different Binary Search Trees which store N different values can be generated.

The input is N, the number of nodes in the tree .

The output is the count of all the uniquely structured binary search trees that can be built from N different values.

Hints:

The number of structurally different Binary Search Trees is not equal with the number of permutations of N numbers. It is possible that two different permutations (representing the order of insertions in the BST) lead to the same structure of a BST. For example, if N=3, and the 3 different values are 4,7,9, the insertion orders (7,4,9) and (7,9,4) generate the same structure of BST.

Design by induction hints:

Implement a recursive version based on the design by induction hints.

Apply dynamic programming techniques and eliminate recursivity.

Cost of Optimal Binary Search Tree

Given a sorted array key [0.. n-1] of search keys and an array freq[0.. n-1] of frequency counts, where freq[i] is the number of searches for keys[i]. Construct a binary search tree of all keys with such a structure such that the total cost of all the searches is minimum. The cost of a BST node is the level of that node multiplied by its frequency. The level of the root is 1.

Read the input from a textfile with the following format: The first line contains N, the number of nodes in the tree The next N lines contain the values of the Keys, in increasing order The next N lines contain the values of the keys frequencies

The output is the value of the cost of the Optimal Binary Search tree that can be built

Hints: Intuitively keys with bigger frequencie must be at lower levels in the tree (near the root).

Design by induction hints: consider the parametrized problem Cost(i,j) which computes the optimal cost of a BST with keys from i to j.

Implement a recursive version based on the design by induction hints.

Apply dynamic programming techniques and eliminate recursivity.

The Maximum Consecutive Subsequence (*)

See also [Manber - 5.8]

Given a sequence X = (x[1], x[2],.., x[n]) of (not necessarily positive) real numbers, find a subsequence x[i]; x[i+1]; ...; x[j] of consecutive elements such that the sum of the numbers in it is maximum over all subsequences of consecutive elements

Examples: in the sequence (2, -3, 1.5, -1, 3, -2, -3, 3) the Maximum Consecutive Subsequence (MCS) is (1.5; -1; 3) having the sum 3.5. If all the numbers in the sequence are negative, then the MCS is empty.

Hints:

The base case: if n=1: if the number is nonnegative, the MCS is that single number; otherwise the MCS is empty.

Try to construct an induction hypothesis: We know how to find the MCS for sequences of sizes smaller than n. How can we use this knowledge to find MCS for sequences of size n ? For a sequence x[1] ... x[n] its MCS is either the MCS of the sequence x[1]...x[n-1], or a new MCS formed by the maximum suffix subsequence of X[n-1] appended with element x[n]. We define the maximum suffix subsequence as the maximum subsequence that ends in x[n-1].