CS 2200 - Program #7 - 20 points
Due Aug 1, 2001 (Yes, Thursday)
Dynamic Programming

Suppose that you want to create a binary search tree in which the frequency of searching for each node is known. Further, the frequency of falling off the tree at each external node is known. If we built the BST with the frequencies in mind, we could build a tree that is more efficiently searched.

Let p[i] be the probability that node i is searched for. Let gone[i] be the probability that you were looking for something greater than node i but less than node i+1; gone[0] is the probability you were searching for something less than the first node.

Consider the following data (in which probabilities are represented as percents):


tabular29

Which of the two trees below (in which probabilities are shown in parentheses) represents the optimal BST?

tex2html_wrap102

You can figure which is better by multiplying the depth (counting the root as 1) times the probability. Doing this we see that the average number of compares for each node is 2.8 for tree (a) and 2.75 for tree (b).

Formally the problem is: we are given a sequence K = {tex2html_wrap_inline68} of n distinct keys in sorted order. In our case, we will assume the keys are consecutive integers beginning at 1. We wish to build a BST for these keys. For each tex2html_wrap_inline70, we have a probability of tex2html_wrap_inline72 that a search will be for tex2html_wrap_inline70. Some searches may not be for members of K, so we have n+1 ``dummy keys'' {tex2html_wrap_inline76} representing values not in K. In particular, tex2html_wrap_inline78 represents all values less than tex2html_wrap_inline80 and tex2html_wrap_inline82 represents all values greater than tex2html_wrap_inline84. For each dummy key tex2html_wrap_inline86 we have probability tex2html_wrap_inline88.

To characterize the optimal substructure of optimal binary search trees, we start with an observation about subtrees. Consider any subtree of a binary search tree. It must contain keys in a contiguous range tex2html_wrap_inline90 and dummy keys tex2html_wrap_inline92. The usual cut and paste argument applies here. If there were a subtree T" whose expected cost is lower than T', we could cut T' out and paste T" in, resulting in a BST of lower expected cost. The fact that an optimal BST consists of optimal subtrees allows us to use a smaller instance of the same problem is solving the whole problem.

Thus, an optimal subtree consists finding a root (with optimal subtrees) such that the tree has the smallest expected cost over all such trees.

Recursion

With recursion, we could try all solutions. A method which looks at all possibilities to find the best method is called an exhaustive method. The term exhaustive doesn't mean you will get tired computing it (although you will), but that you exhaust all possibilities.

For each of the input data, use recursion to find the tree of optimal expected search time and print the tree prettily. The input is found in file prog7.in. Feel free to alter the input format any way you desire.

Memoizing

Memoizing refers to writing yourself a memo saying, ``Hey, don't compute this again! You've already done it once.''

For each input tree, rerun the recursion with the following change. Keep a two-dimensional table Results best[][] which stores the previous results of the recursive calls. For example

class Results{
  int expectedSearchTime; // best possible expected search time
  int root;               // root of the tree giving the best expected search time
  int ctUsed;             // Number of times solution has been used, -1 if not yet computed
}

As you compute the expected search time of the best possible tree, instead of recomputing the subproblems multiple times, you will store the value of the best tree (along with its root). Then when another computation needs the same value, you will just pull the value from the static table rather than having to repeat the recursion. Use the field ctUsed to keep track of how many times each subproblem is used.

Print the contents of this two dimensional table.

Also, print the optimal tree. Notice that while the optimal tree is not actually stored, it is easily built from the information which is stored.

Dynamic Programming

In some recursive solutions, work is increased because you keep solving the same subproblem.

The idea of dynamic programming is to save all the solutions to the subproblems so you can reuse them. You can store the solutions in any way you want. When I solved the problem, I declared an array Result best[begNode][endNode] which stored information about the best possible solution to a tree from begNode to endNode. This arrangement was helpful to me, because then I could turn one problem into instances of smaller subproblems.

Another difference in a dynamic programming solution is that you normally start with the smallest subproblem and determine the solutions for larger and larger subproblems (in terms of the smaller ones). This problem is a bit different in that you don't fill in the array by rows or columns, but follow along a diagonal. For example, you would find solutions for best[1][1] best[2][2]... best[10][10] before computing the next diagonal of the matrix best[1][2], best[2][3] .. best[9][10]. This is different from a recursive solution in which you start with the largest values first.

Using dynamic programming techniques, write the code to compute the optimal selection.

Notice that with dynamic programming, you save the results so you don't have to recompute the solution to the same subproblem multiple times. The saving of partial results is even more beneficial if you can answer multiple questions using the same matrix. However, even if you only need the solution to one problem, dynamic programming is effective for many problems.

Submission

Turn in a listing of your code.

For each BST tree, output:

  1. Print the tree optained by recursion.
  2. Print the best table obtained by memoizing - including the usedCt.
  3. Print the best table obtained by dynamic programming. Print the tree obtained by dynamic programming. Do not simply use the one computed for recursion. Actually create the tree from the dynamic programming tables.

Report

Prepare a report which answers the following questions:

  1. For each input file, compared to recursive, is dynamic programming helpful? Justify your answer.
  2. In general, when is dynamic programming better than recursive?
  3. How many hours did you spend on this problem?

Hints

You should notice a great deal of similarity among the tree solution types. I would recommend getting the recursive solution finished first before starting the others as it was easiest for me.

In all the solutions, you need to figure out how you compute the expected cost of a tree given the expected costs of its subtrees. This is typical of recursive problems - you need to figure out how to put the pieces together.

When you want to find the expected cost of a tree containing nodes tex2html_wrap_inline70... tex2html_wrap_inline84 given you have selected tex2html_wrap_inline111 as the root, you know that the subtrees of tex2html_wrap_inline111 are the optimal subtrees formed from tex2html_wrap_inline115 and tex2html_wrap_inline117. However, you need to know the expected cost of the whole tree from the costs of the smaller trees.

Consider the example, from tree (b) above. The subtree rooted at 5 has cost S= 1*20 + 2*10 + 2*10 + 3*5 + 3*5 + 4*5 + 4*5 (in which each term is the level of the node times the weight of the node). When the subtree rooted at 5 becomes a child of 2, it has the cost S1= 2*20 + 3*10 + 3*10 + 4*5 + 4*5 + 5*5 + 5*5 as everything is just one higher level. Note that we can compute S1 from S in the following way: S1 = S + 20 + 10 + 10 + 5 + 5 + 5 + 5. In other words, we take the original cost of the subtree, and just add all the weights in the tree one more time.

In coding dynamic programming, I found it helpful to have a function int getbest(k, sub) which just returned gone[sub] if subscripts were illegal, and best[k][sub] otherwise. It saved making lots of checks in the body of the code.

Also, I found it helpful to precompute the sum of all weights between i and j = gone[i-1] + p[i] + gone [i] + ... p[j] + gone[j]. This was useful because when the subtree containing nodes i through j became a subtree, I had to increment the expected search cost by the sum of the weights between i and j (as the depth of every node would now be one greater). The code I used to compute the weights is as follows:

   for (j=1, j<SIZE+1;j++)
       w[j][j-1] = gone[j-1];

    for (i=1; i < SIZE;i++)
       for (j=i; j < SIZE;j++)
            w[i][j] = w[i][j-1]+p[j]+gone[j];



Vicki Allan
Fri Jul 26 09:33:28 MDT 2002