一、问题描述
1、问题描述:
基于统计先验知识,我们可统计出一个数表(集合)中各元素的查找概率,理解为集合各元素的出现频率。比如中文输入法字库中各词条(单字、词组等)的先验概率,针对用户习惯可以自动调整词频——所谓动态调频、高频先现原则,以减少用户翻查次数。这就是最优二叉查找树问题:查找过程中键值比较次数最少,或者说希望用最少的键值比较次数找到每个关键码(键值)。为解决这样的问题,显然需要对集合的每个元素赋予一个特殊属性——查找概率。这样我们就需要构造一颗最优二叉查找树。
n个键{a1,a2,a3…an},其相应的查找概率为{p1,p2,p3…pn}。构成最优BST,表示为T1n ,求这棵树的平均查找次数C[1, n](耗费最低)。换言之,如何构造这棵最优BST,使得C[1, n] 最小。
二、动态规划算法解题思路
动态规划法策略是将问题分成多个阶段,逐段推进计算,后继实例解由其直接前趋实例解计算得到。对于最优BST问题,利用减一技术和最优性原则,如果前n-1个节点构成最优BST,加入一个节点 an 后要求构成规模n的最优BST。按 n-1, n-2 , … , 2, 1 递归,问题可解。自底向上计算:C[1, 2]→C[1, 3] →… →C[1, n]。为不失一般性用C[i, j] 表示由{a1,a2,a3…an}构成的BST的耗费。其中1≤i ≤j ≤n。这棵树表示为Tij。从中选择一个键ak作根节点,它的左子树为Tik-1,右子树为Tk+1j。要求选择的k 使得整棵树的平均查找次数C[i, j]最小。左右子树递归执行此过程。(根的生成过程)
引用书上求解递归式:
以输入节点概率 :0.10 0.15 0.20 0.25 0.30为例
在计算C[ i ] [ j ]是需要从上往下按对角线计算,:
下面我们来计算C [ 1 ] [ 2 ] :
1<=i<=j<=2;
I<=k<=j;
三、解题思路图形化
下面介绍一种更为直观的三角形计算方法,其实本质没有发生改变,只是把数学公式图形化,方便填表。
求C[i][j]时,设以C[i][i-1],C[j+1][j],C[i][j]三个点形成的直角三角形的次斜边上的值累加和为S(斜),直角边上对应两点的和为S(直),显然S[直]有j-i+1个,则C[i][j]=S[斜]+min{S[直]}。更方便的理解为将最优树划分为左右子树,寻求其最小和值(构建最优二叉树),同时也能由k值确认最优的根节点值。
下面举个例子求C[1][3]的值,如图所示:
S(斜)=0.1+0.2+0.4=0.7,S(直)=min{S1(直),S2(直),S3(直)}=min{0+0.8,0.1+0.4,0.4+0}=0.4,则C[1][3]=0.7+0.4=1.1。同理,求C[[2]][[4]],如下图所示,C[[2]][[4]]=1.4:
四、思考:为什么输入概率相同(无序),输出结果不一样
总结一句话就是:输入数据是有序的,12345分别代表其根值大小,即第一个输入概率对应的值是1,第二个是2,查找的时候并不是根据概率查找,而是根据根值来构造最优二叉查找树。下面详细分析:
- 输出树不一致
遇到的问题是输入同样的数据,就是各个概率顺序不一样,为什么得到的最优二叉查找树不一样。
例如:每个节点对应的查找概率值:0.10 0.15 0.20 0.25 0.30
那么最优二叉树画出来就是:
但是当输入概率是:0.25 0.10 0.15 0.20 0.30
那么最优二叉树画出来就是:
-
从这两个图可以看出来,它们的最优平均查找次数不一样,连树画出来也不一样。 刚开始我是考虑怎样最优二叉树的查找,如果按照我刚开始的理解就是按照概率查找,其实是错的。那么按照概率查找,第二种二叉树的画法就出错了,因为从根节点0.15 看,它的左右子节点都比它大,下一步就不知道该查找那边的节点了。
-
那么我想了一种解决方法,就是像图一的二叉树一样,将二叉树的概率进行升序排序,那么它的查找树就会符合最优二叉树的查找方法,它的左边节点全部小于根节点,右边的左根节点都大于根节点。如果一定要排序的话那么就说明并不具有通用性,这个动态规划的查找最优二叉树的算法就出错了。
-
那么我就开始考虑是否是我错了,算法算出的每一种结果都是对的,然后我就将所有概率换成了根节点。经过多次的结果验证,发现每个图都符合最优二叉树查找的规律,经过仔细的思考我终于理解了:因为查找的键值是根据根节点,而不是根据概率,而且每个键值都是升序。就像跟几点是1,2 ,3 ,4 ,5,那么它的键值大小排序就是1<2<3<4<5,这也就解释了求概率矩阵的时候为什么c[ I ] [ j ]是i到j时连续的,而不是跳变的。所以这个问题隐藏了一个条件,就是输入的概率对应的键值是升序,那么第二个最优二叉树应该是:
流程图
详细流程图
实例
输入输入概率:0.25 0.10 0.15 0.20 0.30
对应最优二叉查找树:
示例代码
C++写的代码:
//最优二叉查找树
#include<bits/stdc++.h>
using namespace std;
//const int maxval = 9999;
double BST(int n,double p[],double c[][100],int r[][100])
{
for(int i=1;i<=n;i++)
{
c[i][i-1]=0; //Ci矩阵初始化
c[i][i]=p[i];
r[i][i]=i; //R根矩阵初始化
}
c[n+1][n]=0;
for(int d=1;d<n;d++) //安对角线计算,从第二条对角线开始
{
for(int i=1;i<=n-d;i++) //行的取值范围
{
int j=i+d; //j求出在对角线上的i对应的
double minval=9999;
int mink=i; //最小值对应根点
double sum=0;
for(int k=i;k<=j;k++)
{
sum=sum+p[k];
if(c[i][k-1]+c[k+1][j]<minval) //三角形比较法,选最小值
{
minval=c[i][k-1]+c[k+1][j];
mink=k;
}
}
c[i][j]=minval+sum;//得到了最小值
r[i][j]=mink;//记录取得最小值时的根节点
}
}
return c[1][n];
}
int main()
{
while(1)
{
cout<<"input operand : 1 enter ,2 exit system"<<endl;
int ch;
cin>>ch;
if(ch==2)
return 0;
else if(ch==1)
{
system("cls");
int n;
cout << "input the point number" << endl;
cin>>n; //节点个数
double p[n]; // 概率数组
memset(p,0,sizeof(p));
// 将概率数组排序,保证正确
cout<<"input each point probability" <<endl;
for(int i=1;i<=n;i++)
{
cin>>p[i];
}
double c[n+2][100]; //动态ci矩阵
int r[n+2][100]; //根矩阵
memset(r,0,sizeof(r));
memset(c,0,sizeof(c));
double s=BST(n,p,c,r);
cout << "the minimum compare times is " << s<<endl;
cout << "the minimum probability matrix is" << endl;
for(int i=1;i<=n+1;i++)
{
for(int j=0;j<=n;j++)
{
cout<<std::setw(3);
cout << c[i][j] << " ";
}
cout << endl;
}
cout << "the point matrix " << endl;
for(int i=1;i<=n+1;i++)
{
for(int j=0;j<=n;j++)
{
cout << r[i][j] << " ";
}
cout << endl;
}
}
}
}