动态规划之最优二叉搜索树

来源:互联网 发布:优化非常好的大型单机 编辑:程序博客网 时间:2024/05/16 09:49

在看这张之前,最好看看我写的动态规划详解,里面都是讲理论基础,我下面的分析都是在此基础上进展的。


给定一个由n个互异的关键字组成的序列K = (k1, k2, ……, kn),且关键字有序(因此有k1 < k2 < …… < kn),从这些关键字中构造一棵二叉查找树。对每个关键字ki, 一次搜索为ki的概率是pi。某些搜索的值可能不在K内,因此还有n + 1个“虚拟键”d0, d1, d2, ……dn代表不在K内的值,且ki ≤ di ≤ k(i+1),di概率为qi。
      因为搜索每个关键字的概率不同,因此最优二叉查找树即一棵期望搜索代价最小的二叉查找树。


      从下图a), b)中可以看出,最优二叉查找树并不是高度最低的树,因为第一棵树期望是2.80,而第二棵是2.75。公式(15.10)显而易见。

假设一次搜索的实际代价为检查的结点个数,亦即,在T内搜索所发现的结点的深度加上1(这里深度从0开始算), 所以一次搜索的期望代价为公式(15.11),期望 = 深度 * 概率。


下面依旧用DP的四个步骤来分析:
①.描述最优解的结构。

基本原则:从n下手,找递归。

与小的分组产生递归,可否从n个结点中任选一点?可以,因为我们要考察以不同结点为根的情况,所以可以假定任何一个元素为根节点。由于我们要构造的是二叉查找树,左子树小于根,根小于右子树,排列有一定的规律。比如我们选择了k为根节点,那么p的1→k-1以及q的0→k-1都在k结点的左边,其余都在k结点的右边。我们对1→k-1再进行选取中间节点i递归的进行时发现,分成了1→i-1和i+1→k-1,数据的表示范围左右都在变化。因此设置二维数组e[i,j]来表示概率和。


②.递归定义最优解的值。

我们在i...j中选取任意一点k为根节点,两边的分别构成左右子树,这里我们强调一下,为了简便,将e[i,j]设定为一棵完整的树,其根节点就表示树根,不是某棵树的子树根,而其深度的差异包含在父节点划分子结构的代价里面。我们可以写出e[i,j]=e[i,k-1]+e[k+1,j]+w(i,j)。这里的w(i,j)如何求?不仅包含了根节点k的概率,还包含了由于i...k-1和k+1...j因为深度加1。当一棵树成为一个结点的子树时,它的搜索代价的变化:子树中每个结点的深度增加1,由公式(15.11),这个子树的期望搜索代价增加为子树中所有概率的总和。因此w(i,j)=pi+...pj + qi-1 +...+qj。

考虑边界条件,由于q为未找到的概率,当i=j时表示查找到,当j=i-1时表示未找到。所以边界条件为j=i-1时,e[i,j]=qi-1。


但是这里必须注意,我们设二维数组时必须考察数组的范围,因为q由0...n,必须能够满足让i>j。把e[i, j]保存在表e[1……n + 1, 0……n], 第一维的下标需要达到n + 1而不是n, 原因是为了有一个只包含虚拟键dn的子树,需要计算和保存e[n + 1, n]。

③.按自底而上的方式计算最优解的值。

再考虑如何自底向上求解。由于r在i...j内变动,其范围也是有长度决定的,类似于矩阵链相乘。所以对e[i,j]遍历的时候按照长度来算。为了寻求进一步的优化,也可以对w[i,j]进行自底向上。w[i,j]=w[i,j-1]+pj+qj。当j=i-1时,w=qi-1。

④.由计算出的结果创造一个最优解。

为了能够重构最优解,保存分割点r,因此设置同样的二维数组root[1...n+1,0...n],当遍历时更新e[i,j]时一同更新root[i,j]=r。



第5, 6行两层循环:
第一次迭代中,l = 1, 循环计算e[i, i]和w[i, i],对于i = 1, 2, ……, n。
第二次迭代中,l = 2, 循环计算e[i, i + 1]和w[i, i + 1], 对于i = 1, 2, ……, n – 1;
第三次迭代中,  l = 3, 循环计算e[i, i + 2]和w[i, i + 2], 对于i = 1, 2, ……, n – 2;
     如果观察下边图,可以发现,它是自底向上一层一层往上迭代。看似像树的分布实际上是由表旋转而来,原来的可以看箭头所指的图(旋转了45°), 9~13行即利用(15.14)的公式跟新root和e,类似文章刚开始的数塔。





代码实现:

/*e[i][j] :0.05 0.45 0.90 1.25 1.75 2.75     0.10 0.40 0.70 1.20 2.00          0.05 0.25 0.60 1.30               0.05 0.30 0.90                    0.05 0.50                         0.10 w[i][j]:0.05 0.30 0.45 0.55 0.70 1.00     0.10 0.25 0.35 0.50 0.80          0.05 0.15 0.30 0.60               0.05 0.20 0.50                    0.05 0.35                         0.10 root[i][j]:1 1 2 2 4  2 2 2 4    3 4 5      4 5        5*/#include<iostream>#include<cstdio>#include<climits>using namespace std; //以5个元素为例 //k1 < k2 < k3 < k4 < k5double p[6] = {0, 0.15, 0.1, 0.05, 0.1, 0.2};double q[6] = {0.05, 0.1, 0.05, 0.05, 0.05, 0.1}; double e[10][10], w[10][10];int  root[10][10]; const int N = 5; void OptimalBST(int n) {    for (int i = 1; i <= n + 1; i++) {        e[i][i - 1] = q[i - 1];        w[i][i - 1] = q[i - 1];    }    for (int l = 1; l <= n; l++) {        for (int i = 1; i <= n - l + 1; i++) {            int j = i + l - 1;            e[i][j] = INT_MAX;            w[i][j] = w[i][j - 1] + p[j] + q[j];            for (int r = i; r <= j; r++) {                double t = e[i][r - 1] + e[r + 1][j] + w[i][j];                if (t < e[i][j]) {                    e[i][j] = t;                    root[i][j] = r;                }            }//for(r)         }//for(i)     }//for(l) } int main() {     OptimalBST(N);     puts("e[i][j] :");     for (int i = 1; i <= 6; i++) {        for (int j = 0; j <= 5; j++) {            if (!e[i][j]) {                printf("     ");                continue;            }             printf("%.2f ", e[i][j]);        }puts("");     }      puts("\nw[i][j]:");      for (int i = 1; i <= 6; i++) {        for (int j = 0; j <= 5; j++) {            if (!w[i][j]) {                printf("     ");                continue;            }             printf("%.2f ", w[i][j]);        }puts("");     }           puts("\nroot[i][j]:");     for (int i = 1; i <= 5; i++) {        for (int j = 1; j <= 5; j++) {            if (!root[i][j]) {                printf("  ");                continue;            }             printf("%d ", root[i][j]);        }puts("");     }     return 0;}












0 0