[Introduction to algorithms] "optimal binary search tree" for Dynamic Planning
The previous two Articles respectively talked about Dynamic PlanningSteel Pipe Cutting"And"Matrix chain multiplication", I feel this article can also be regarded as an ending. In fact, based on the first two articles, we can also make some conclusions here. We can find some regular things.
The so-called dynamic planning is actually to solve the problem of repeated computing subproblems in recursive calls, resulting in a lot of time and repetitive work. The solution is to save the results of the repeated sub-problems first, and then use them directly when necessary, instead of computing.
However, pay attention to the following points:
① To solve the problem, such as the long steel pipe in the "Steel Pipe Cutting", the optimal solution should be able to be a combination of the optimal solutions to the sub-problems decomposed.
② How to provide a unified recursive call form for optimal resolution of sub-problems and Sun Tzu problems.
③ Find one or several suitable sets to save the solutions to the sub-problems we are seeking, or to divide the solutions. For example, in Steel Pipe Cutting, we use an array. In matrix chain multiplication, we use a two-dimensional array.
④ Use the top-down or bottom-up scheme.
I. Overview
Well, let's get down to the truth and look at the optimal binary search tree. For more information about the Binary Search Tree, see the previous two articles:Implement non-recursive traversal of the forward, middle, and backward orders of Binary TreesAndInsert and delete Binary Search Trees. I will not go into details here.
Suppose we want to design a program to translate English text into French. We can create a binary search tree that uses n English defects as keywords and corresponding French words as associated data. Then we traverse the English words of each node in the binary search tree in sequence to find the appropriate words for translation. Here we certainly hope that the less time for this search, the better. If we only consider the time complexity, we can consider using the red and black trees to achieve the Olg (n) search time complexity ). However, for English words, the probability of each word appearing is different, such as ". Obviously, the closer this type of Word to the root node, the less time it takes to search. For some rare words, you can keep them away from the root node.
The problem is that we know n different keywords.KiAnd our probability of appearanceP [I], How do we make up suchOptimal Binary Search TreeWhat about it?
The result of searching for this binary tree is similar to two types. One is to find the desired key value, and the other is to traverse the entire tree without finding the key value, we will arrange some node di for representation if the key value is not found, so it is clear that di nodes are all leaf nodes. It is called di as a pseudo keyword and the probability isQ [I]. D0 indicates all values smaller than k1, dn indicates all values greater than kn, and other di (1 <= I <= N-1) indicates that the di value is between ki and ki + 1. Because a search involves either di or ki, all of which include:
Now we can give an expectation for the number of nodes that will be searched at a time, because for Binary Trees, each layer will find one more node, therefore, we can add 1 to the depth of each node, multiply the pi of the node, and sum the results of all nodes to find an expectation of the node that will be searched by a tree:
The optimal binary search tree is defined as the binary search tree structure with the minimum expectation for a given node key value probability.
Ii. Use Dynamic Planning to process the optimal binary search tree
The steps are as follows:
① First, divide the problem into subproblems to see if the problem is optimal or not.
You can use the "jianqie-paste" method to prove that for an optimal binary search tree T, for a subtree Ti of T, assume that it contains the keyword ki .... kj. If Ti is not the optimal binary query tree, it means that another tree contains the node ki... the binary search tree Tj of kj makes Tj's search expectation lower than Ti's, so we can "cut" Ti from t, then we paste Tj to the original Ti location. As a result, we will find that the newly generated tree is definitely lower than the search expectation of the original tree T. This is a contradiction, we suppose T is an optimal binary search tree. Therefore, it can be proved that the subtree of the optimal binary search tree is the optimal binary search tree.
Therefore, we can determine that the problem of the optimal binary search tree can be broken down into sub-problems, and the sub-problems are also the problem of the optimal binary search tree, which can be easily met, there will be multiple repeated sub-problems, Sun Tzu problems, and so on. Therefore, dynamic planning can be used to solve dynamic planning conditions.
② The second thing to solve is the unified form of Optimal Solution for Recursive subproblems (computational formula)
At this time, it is similar to the previous "optical tube cutting" and matrix chain multiplication. It is easy to see that we want to break down the search expectation Problem of a tree: the search expectation problem of the Left subtree + the search expectation of the root node + the search expectation of the right subtree; for the root node, its depth is 0, so use it directly (0 + 1) * p [root location] is the expectation of search. For left and right subtree, if they are a separate tree, you can also use the form (left subtree search expectation problem + root node search expectation + right subtree search expectation) to recursion directly, but here, left and right Subtrees have a layer more depth than simple binary trees, because they have an additional root node on their heads, therefore, we need to add each node to their own probability of occurrence p [I] or q [I] (including keyword nodes and pseudo keyword nodes ), the increase is the sum of the probabilities of all nodes in each tree. W (I, j) indicates the tree containing the node ki... kj. The sum of probabilities of all nodes is as follows:
If we use the node at r as the root node, there are:
Based on the above analysis, we will search for a tree, assuming that the root node is at Kr, probability of decomposition into root node p [r] And left subtree search expect addition of left subtree node probability and: e (I, R-1) + w (I, R-1 ), in the right subtree search, we expect to add the node probability of the right subtree and e (r + 1, j) + w (r + 1, j ).
Here we also want to consider a situation, that is, e (I, j), when j = I-1, then the subtree cannot contain keywords, because the number of the keyword I <= k <= j, and j = I-1, obviously not satisfied. So then the subtree will only contain a pseudo keyword node: di-1.
Based on the above two formulas, there are:
In this way, the unified recursive form of the subproblem we need is obtained.
③ Select an appropriate data structure to store the processing results and partition information of sub-problems.
Here we need to save the following data: the optimal search expectation value e (I, j) for each subtree, and the root node location r (I, j) for each subtree at the best time ), and the probability and value of each subtree: w (I, j ).
④ Use a bottom-up approach to solve child and parent issues.
Therefore, according to the previous "Matrix chain multiplication" idea, it is easy to give the following code:
Const int MaxVal = 0x7fffffff; const int n = 5; // probability of searching for the root node and virtual key double p [n + 1] = {-1, 0.15, 0.1, 0.05, 0.1, 0.2}; double q [n + 1] = {0.05, 0.1, 0.05, 0.05, 0.05, 0.1 }; int root [n + 1] [n + 1]; // record the root node double w [n + 2] [n + 2]; // subtree probability sum double e [n + 2] [n + 2]; // The expected cost of the subtree/** p is an array storing the node probability of a keyword [1, n], q is an array that stores the probability of occurrence of pseudo-Keyword nodes (leaf nodes) [0, n] * n is the number of nodes * e [I, j] is the storage of Key Words Containing Ki ~ The expected cost of one query in the optimal binary book of Kj is as follows: q [n] and e [] q [0] of e [n + 1, n]. therefore, the range is [0, n + 1] * w [I, j], which stores Ki ~ Probability sum of Kj, w [I, j] = w [I, r-1] + p [r] + w [r + 1, j] * root [I, j] is to store Ki ~ Subtree of the key word of Kj. in optimal conditions, the root node subscript */void optimal_Bst (double * p, double * q, int n) {// first processes w [I, in j] and e [I, j] I = j + 1, this case is q [I-1] for (int I = 1; I <= n + 1; I ++) {w [I] [I-1] = q [I-1]; w [I] [I-1] = q [I-1];} // then process the cyclic int L from 1 to n with the length of l = 0; // representing the ki ~ The length of kj is int j = 0; // It indicates the lower mark value of the last element, jint I = 0; // It indicates the lower mark value of the starting element, iint r = 0; // represents the root node subscript double tmp = 0; // here, the temporary result of the e array element calculation is stored, so it is also of the double type for (l = 1; l <= n; l ++) {// take I as the outer loop. Here, because the length is L, the maximum I value is n-l + 1, j-I + 1 = L, j = L + i-1for (I = 1; I <= n-l + 1; I ++) {j = l + I-1; // initialize e [I] [j] AND w [I] [j] e [I] [j] = MaxVal; w [I] [j] = w [I] [j-1] + p [j] + q [j]; for (r = I; r <= j; r ++) {// formula: e [I] [j] = e [I] [r-1] + e [r + 1] [j] + w [I] [j]; tmp = e [I] [r-1] + e [r + 1] [j] + w [I] [j]; if (tmp <e [I] [j]) {e [I] [j] = tmp; root [I] [j] = r ;}}}}}
The time complexity of the above algorithm is O (n ^ 3). We can make some optimizations to change it to O (n ^ 2). Here we need to draw a conclusion:
The optimal_Bst function is changed to the following form to change the Traversal method of the root node location r. Of course, the judgment conditions should also be added before:
/** Optimized version */void optimal_Bst2 (double * p, double * q, int n) {// process w [I, j] and e [I, in j] I = j + 1, this case is q [I-1] for (int I = 1; I <= n + 1; I ++) {w [I] [I-1] = q [I-1]; w [I] [I-1] = q [I-1];} // then process the cyclic int L from 1 to n with the length of l = 0; // representing the ki ~ The length of kj is int j = 0; // It indicates the lower mark value of the last element, jint I = 0; // It indicates the lower mark value of the starting element, iint r = 0; // represents the root node subscript double tmp = 0; // here, the temporary result of the e array element calculation is stored, so it is also of the double type for (l = 1; l <= n; l ++) {// take I as the outer loop. Here, because the length is L, the maximum I value is n-l + 1, j-I + 1 = L, j = L + i-1for (I = 1; I <= n-l + 1; I ++) {j = l + I-1; e [I] [j] = MaxVal; w [I] [j] = w [I] [j-1] + p [j] + q [j]; if (I = j) {root [I] [j] = I; e [I] [j] = p [I] + q [I-1] + q [j];} else {// initialize e [I] [j] AND w [I] [j] for (r = root [I] [j-1]; r <= root [I + 1] [j]; r ++) {// formula: e [I] [j] = e [I] [r-1] + e [r + 1] [j] + w [I] [j]; tmp = e [I] [r-1] + e [r + 1] [j] + w [I] [j]; if (tmp <e [I] [j]) {e [I] [j] = tmp; root [I] [j] = r ;}}}}}}
After the preceding modification, the time complexity of O (n ^ 2) can be reached.
Exercise 15.1-1: a function that prints the relationship between each node in the above optimal binary search tree and its parent node:
/** Print the optimal binary search tree */void print_optimal_Bst (int I, int j, int r, bool root_flag) {int root_node = root [I] [j]; if (root_flag) {cout <"root node: k" <root_node <endl; root_flag = false; print_optimal_Bst (I, root_node-1, root_node, root_flag ); print_optimal_Bst (root_node + 1, j, root_node, root_flag); return;} if (I> j + 1) {return;} else if (I = j + 1) {if (j <r) {cout <"d" <j <"yes" <"k" <r <"left child" <endl ;} else {cout <"d" <j <"yes" <"k" <r <"right child" <endl;} return; // here, a return is added because when the loop reaches I = j + 1, it is already in the header and cannot be recursive, now there is no reasonable root_node} else {if (root_node <r) {cout <"k" <root_node <"yes" <"k" <r <"left child" <endl ;} else {cout <"k" <root_node <"yes" <"k" <r <"right child" <endl ;}} print_optimal_Bst (I, root_node-1, root_node, root_flag); print_optimal_Bst (root_node + 1, j, root_node, root_flag );}
Output result: