/* 最优二叉搜索树:给定一个n个不同关键字的已经排序的序列K=<k1,k2,...,kn>(因此k1<k2<...<kn),希望用这些关键字构造一颗二叉搜索树。 对每个关键字ki,都有一个概率pi表示其搜索频率。有些要搜索的值可能不在K中,因此我们还有n+1个“伪关键字”do,d1,d2,...,dn表示不 在K中的值。d0表示所有小于k1的值,dn表示所有大于kn的值,对i=1,2,...,n-1,伪关键字di表示所有在ki和ki+1之间的值。对于每个伪关 键字di,也都有一个概率pi表示对应的搜索频率。下图显示了n=5个关键字的集合构造的两颗二叉搜索树。每个关键字ki是一个内部结点, 而每个关键字di是一个叶结点。每次搜索要么成功(找到某个关键字ki),要么失败(找到某个伪关键字di),因此有如下公式: i属于[1,n] pi的和 + i属于[0,n] qi的和 = 1 关键字ki的两个伪关键字为di-1,di k2 k1 k4 d0 d1 k3 k5 d2 d3 d4 d5 动态规划分析: S1:最优二叉搜索树的结构 二叉搜索树的任意子树,必须包含连续关键字ki,...,kj, 1<=i<=j<=n,而其叶节点必然是伪关键字di-1,...,dj。 最优子结构:如果一颗最优二叉搜索树T有一颗包含关键字ki,...,kj的子树T',那么T'必然是包含关键字ki,...,kj和伪关键字di-1,...,dj的 子问题的最优解。 剪切-粘贴法证明:如果存在子树T'',使得T''的期望搜索代价比T'的期望搜索代价要小,那么将T'从T中删除,将T''粘贴到该位置,就得到一颗 期望搜索代价比T的期望搜索代价更小的树,这与T是最优二叉搜索树矛盾,所以T'是子问题的最优解,即最优二叉搜索树有 最优子结构 最优子结构---->可以用子问题的最优解构造原问题的最优解,给定关键字序列ki,...,kj,设其中某个关键字为kr(i<=r<=j),是这些关键字的最 优子树的根节点。那么kr的左子树就包含关键字ki,...,kr-1(和伪关键字di-1,...,dr-1),而右子树包含关键字kr+1,...,kj (和伪关键字dr,...,dj)。只要我们检查所有可能的根节点kr(i<=r<=j),并对每种情况分别求解包含ki,...,kr-1及kr+1,..., kj的最优二叉搜索树,即可保证找到原问题的最优解 易错点:注意空子树。假设对于包含关键字ki,...,kj的子问题,我们选定ki为根节点,其实有一个伪关键字di-1,若kj为根节点,那么其右子树 包含伪关键字dj S2:一个递归算法 子问题域:求解包含关键字ki,...,kj的最优二叉搜索树,其中i>=1,j<=n且j>=i-1(当j=i-1时,子树不包含实际关键字,只包含伪关键字di-1)。 定义e[i,j]为在包含关键字ki,...,kj的最优二叉搜索树中进行搜索的期望代价。 计算结果:e[1,n] j=i-1的情况最为简单,子树只包含伪关键字di-1,期望搜索代价为 e[i,i-1]=qi-1 公式(1) 当j>=i时,我们需要从ki,...,kj中选择一个根节点kr,然后构造一颗包含关键字ki,...,kr-1的最优二叉搜索树作为其左子树,以及一颗包含关键 字kr+1,...,kj的二叉搜索树作为其右子树。 关键:当一颗子树成为一个节点的子树时,期望搜索代价有何变化?由于每个节点的深度都增加了1,这颗子树的期望搜索代价的增加值为所有概率 之和。对于包含关键字ki,...,kj的子树,所有概率之和为: w(i,j)=l从i到j累加pl + l从i-1到j累加ql 公式(2) 因此,若kr为包含关键字ki,...,kj的最优二叉搜索树的根节点,有如下公式: e[i,j]=pr + ( w[i,r-1] + e[i,r-1] ) + ( w[r+1,j] + e[r+1,j] ) 公式(3) w(i,j) = pr + w(i,r-1) + w(r+1,j) 公式(4) 由上述两个公式得到e[i,j] = w(i,j) + e[i,r-1] + e[r+1,j] 公式(5) 递归公式 e[i,j]={qi-1 ,若j=i-1 公式(6) {min i<=r<=j { e[i,r-1] + e[r+1,j] + w(i,j) } ,若i<=j 用root[i,j]保存根节点kr的下标r S3:计算最优二叉搜索树的期望搜索代价 用表保存结果 关键: e[1...n+1, 0...n]来保存e[i,j]值。 第一维下标上街为n+1而不是n,因为只含有伪关键字dn的子树,需要计算e[n+1,n],看n+1在第一维度 第二维下标下界为0,是因为对于只包含伪关键字d0的子树,需要计算e[1,0],看0在第二维度 还需要一个表提高计算效率,为了避免每次计算e[i,j]时都重新计算w(i,j),我们将这些值保存在表w[1...n+1, 0...n]中。 w[i,i-1]=qi-1 (1<=i<=n+1) 公式(7) w[i,j]=w[i,j-1]+pj+qj (j>=i) 公式(8) 公式(8)来源于对公式(4)中带入r=j得到,这个不容易想到 输入关键字个数n,接下来有两行,第一行是n个数字,表示p1~pn,第二行是n+1个数字,表示q0~qn 输出最优二叉搜索树的期望搜索代价 输入: 5 0.15 0.1 0.05 0.1 0.2 0.05 0.1 0.05 0.05 0.05 0.1 输出: 2.75 关键: 1 对于含有长度累加递增的递归公式可以转化为递推,也就是要熟悉下面这种计算方式 即计算的是这种: 第一趟:e[1,1],e[2,2],...,........,e[n,n] 第二趟:e[1,2],e[2,3],...,e[n-1,n] ... 第n趟 :e[1,n] 观察可发现,每趟使得两个下标之间的差值从0~n-1 因此分析差值的范围:0~n-1 对应i的范围由j来产生的 因为我们知道 j=i+l-1 <= n 因此得到 i <= n - (l-1) 2 所有搜索概率为浮点数,因此 double p[N+1]; double optimalBinarySearchTree(int i, int j, int n) 返回值都是浮点数 3 公式 计算e[1][n] e[i][j]={qi-1 , j=i-1 {min i<=r<=j { e[i][r-1] + w[i][j] + e[r+1][j] } w[i][i-1]=q[i-1] w[i][j]=w[i][r-1] + pr + w[r+1][j] w[i][j]=w[i][j-1] + pj + qj , j >= i 4setiosflags(ios::fixed)固定浮点显示, */ #include <iostream> #include <string.h> #include <iomanip> //io输出,manip表示manipulator操纵器 using namespace std; const int N = 100; double e[N+1][N];//实际e[i][j]表示包含关键字ki,...,kj的最优二叉搜索树的搜索期望代价, 由于dn=e[n+1,n],d0=e[1,0],因此e[1...n+1,0...n] double w[N+1][N];//w[i][j]表示包含关键字ki,...,kj的子树成为另一个结点的子树时,子树增加的搜索代价 int root[N][N];//root[i][j]表示包含关键字ki,...,kj的最优二叉搜索树的根节点kr的下标r,因为这里的i和j下标都是1~N,所以用root[N][N]初始化 double p[N+1];//p[i]表示关键字ki搜索概率,注意概率是double类型 double q[N+1];//q[i]表示伪关键字di的搜索概率 /* 计算e[1][n] e[i][j]={qi-1 , j=i-1 {min i<=r<=j { e[i][r-1] + w[i][j] + e[r+1][j] } w[i][i-1]=q[i-1] w[i][j]=w[i][r-1] + pr + w[r+1][j] w[i][j]=w[i][j-1] + pj + qj , j >= i */ //计算搜索代价增加数组 void getW(int n) { //初始化w和e矩阵中的值为特殊值 for(int i = 1; i <= n+1 ; i++) { for(int j = 0; j <= n ; j++) { w[i][j] = -1; e[i][j] = -1; } } //初始化搜索代价增加值 for(int i = 1 ; i <= n+1 ; i++) { w[i][i-1] = q[i-1]; //初始化e e[i][i-1] = q[i-1]; } //用递推公式,计算搜索代价增加数组的其他值 for(int i = 1 ; i <= n + 1 ; i++) { for(int j = i ; j <= n ; j++) { w[i][j] = w[i][j-1] + p[j] + q[j]; } } } //递归版本,计算最优二叉搜索树的期望搜索代价 double optimalBinarySearchTree(int i, int j, int n) { if(e[i][j] != -1) { return e[i][j]; } else if(j == i-1) { e[i][j] = q[i-1]; return e[i][j]; } else { int iMin = INT_MAX; for(int r = i ; r <= j ; r++) { int iValue = optimalBinarySearchTree(i,r-1,n) + w[i][j] + optimalBinarySearchTree(r+1,j,n); if(iValue < iMin) { iMin = iValue; root[i][j] = r; } } e[i][j] = iMin; return e[i][j]; } } //递推版本,注意返回值也是浮点数 double optBinSeaTree(int n) { //初始化e和w 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 iTemp = e[i][r-1] + w[i][j] + e[r+1][j];//注意是浮点数不是整数 if(iTemp < e[i][j]) { e[i][j] = iTemp; root[i][j] = r; } } } } return e[1][n]; } void process() { int n; while(cin >> n) { for(int i = 1 ; i <= n ; i++) { cin >> p[i]; } for(int j = 0 ; j <= n ; j++) { cin >> q[j]; } double iResult = optBinSeaTree(n); //double iResult2 = optimalBinarySearchTree(1, n, n); cout << setiosflags(ios::fixed) << setprecision(2) << iResult << endl;//setiosflags(ios::fixed)固定浮点显示, //cout << setiosflags(ios::fixed) << setprecision(2) << iResult2 << endl;//setiosflags(ios::fixed)固定浮点显示, } } int main(int argc, char* argv[]) { process(); getchar(); return 0; }