算法实验 最优二叉搜索树

最优二叉搜索树

问题描述

二叉搜索树我们都知道,左子树结点的值都小于根结点,右子树结点的值都大于根节点。如果某个结点没有左子树或右子树,那么在对应的位置上加一个虚结点。现在,给出n个结点的搜索概率 p i p_i pi,以及n+1个虚结点的搜索概率 q i q_i qi(这些结点的值按递增排列),问最优二叉搜索树的搜索代价,以及这棵树的具体构造。
其中,实结点的搜索代价为它的深度+1,虚结点的搜索代价为它的深度。

问题分析

  1. 什么是虚结点,为什么要考虑虚结点?
    给定一棵二叉搜索树,如下图。除了对树中存在的结点的查询需要代价,对一些不存在的点也需要经过判断才能得出它“不在树中”的结果。例如,在下面棵树中,我们查询6这个不存在结点,需要将指针从根节点7移动两次到达结点5,这时才能得出结论,6不存在。所以把这些不存在的点看作是一个虚结点。
    在这里插入图片描述
  2. 如何计算最优二叉树
    我们不能交换树中的两个结点,因为它是一棵“二叉搜索树”,左子树必定小于根节点,右子树必定大于根节点,对结点的交换会破坏这一性质。我们可以做的是,在一个区间内,选择一个结点来成为根节点。例如,现在有个实结点序列,它的查找概率存储在数组p下标[i,j]之间。设 d p [ a , b ] dp[a,b] dp[a,b]代表[a,b]这个区间内,最优二叉搜索树的代价, w [ i ] [ j ] w[i][j] w[i][j]代表区间[i,j]搜索概率之和。那么, d p [ i ] [ j ] dp[i][j] dp[i][j]就等于它左子树的搜索代价 d p [ i ] [ k − 1 ] + w [ i ] [ k − 1 ] ∗ 1 dp[i][k-1]+w[i][k-1]*1 dp[i][k1]+w[i][k1]1(因为区间[i][k-1]成为了结点k的左子树,整体深度+1,那么搜索代价就会增加 w [ i ] [ k − 1 ] ∗ 1 w[i][k-1]*1 w[i][k1]1),加上右子树的搜索代价 d p [ k + 1 ] [ j ] + w [ k + 1 ] [ j ] ∗ 1 dp[k+1][j]+w[k+1][j]*1 dp[k+1][j]+w[k+1][j]1,再加上根节点的搜索代价 w [ k ] [ k ] ∗ 1 w[k][k]*1 w[k][k]1。于是得出下面的状态转移方程:
    d p [ i ] [ j ] = d p [ i ] [ k − 1 ] + d p [ k + 1 ] [ j ] + w [ i ] [ k − 1 ] + w [ k ] [ k ] + w [ k + 1 ] [ j ] = d p [ i ] [ k − 1 ] + d p [ k + 1 ] [ j ] + w [ i ] [ j ] \begin{aligned} dp[i][j]&=dp[i][k-1]+dp[k+1][j]+w[i][k-1]+w[k][k]+w[k+1][j]\\&=dp[i][k-1]+dp[k+1][j]+w[i][j] \end{aligned} dp[i][j]=dp[i][k1]+dp[k+1][j]+w[i][k1]+w[k][k]+w[k+1][j]=dp[i][k1]+dp[k+1][j]+w[i][j]
    d p [ i ] [ j ] dp[i][j] dp[i][j]就是需要求的最终答案,在已知所有子问题的答案时,我们只需遍历一次根节点k所有可以取的位置,就可以求出它了:
    d p [ i ] [ j ] = m i n i < = k < = j ( d p [ i ] [ k − 1 ] + d p [ k + 1 ] [ j ] + w [ i ] [ j ] ) dp[i][j]=min_{i<=k<=j}(dp[i][k-1]+dp[k+1][j]+w[i][j]) dp[i][j]=mini<=k<=j(dp[i][k1]+dp[k+1][j]+w[i][j])
    那么,如何得知所有子问题的答案呢?
    我们发现,子问题为 d p [ i ] [ k − 1 ] dp[i][k-1] dp[i][k1] d p [ k + 1 ] [ j ] dp[k+1][j] dp[k+1][j],( i < = k < = j i<=k<=j i<=k<=j)。它与原问题 d p [ i ] [ j ] dp[i][j] dp[i][j]有着相同的形式,只不过规模更小。不断把子问题划分为“子子问题”,问题的规模会越来越小。直到子问题成为 d p [ k ] [ k ] ( i < = k < = j ) dp[k][k](i<=k<=j) dp[k][k](i<=k<=j),它的意义为在[k,k]这个区间内(只有一个实结点),最优二叉搜索树的代价。这个问题是很容易解决的: d p [ k ] [ k ] = p [ k ] + q [ k − 1 ] + q [ k + 1 ] dp[k][k]=p[k]+q[k-1]+q[k+1] dp[k][k]=p[k]+q[k1]+q[k+1]
    其中 p [ k ] p[k] p[k]代表实结点k的搜索概率, q [ k − 1 ] q[k-1] q[k1] q [ k + 1 ] q[k+1] q[k+1]代表虚结点k-1与k+1的搜索概率。
    所以,最小的子问题可以由输入计算出来,一步步由小问题的答案,推出大问题的答案,就可以得出结果。
  3. 实现方法
    前面的分析是从上往下的,实际编程时,我们要预处理出所有的 w [ i ] [ j ] w[i][j] w[i][j](双重循环,外层为区间长度,内层为区间开端),并且计算出初始值 d p [ k ] [ k ] dp[k][k] dp[k][k]。然后用三层循环求出所有的 d p [ i ] [ j ] dp[i][j] dp[i][j](根据前面的状态转移方程),输出答案 d p [ 1 ] [ n ] dp[1][n] dp[1][n]即可。

代码

#include<iostream>
using namespace std;
const int maxn=1e3;
const double eps=1e-5;
int n,root[maxn][maxn]; //root[maxn][maxn]保存树的结构
double p[maxn],q[maxn],dp[maxn][maxn],w[maxn][maxn];

void dfs(int a,int b){
    if(a>=b)    return;
    printf("%d是子树[%d,%d]的根.\n",root[a][b],a,b);
    dfs(a,root[a][b]-1);
    dfs(root[a][b]+1,b);
}

int main(){
    scanf("%d",&n); //输入实结点个数
    for(int i=1;i<=n;i++)    scanf("%lf",&p[i]);    //实结点
    for(int i=1;i<=n+1;i++)   scanf("%lf",&q[i]);   //虚结点
    for(int i=1;i<=n;i++)    w[i][i]=p[i]+q[i]+q[i+1];
    for(int i=1;i<=n;i++)
        for(int j=1;i+j<=n;j++)
            w[i][i+j]=w[i][i+j-1]+p[i+j]+q[i+j+1];  //预处理w[i][j]
    for(int len=0;len<n;len++)
        for(int i=1;i+len<=n;i++)
            for(int j=i;j<=i+len;j++)
                if(dp[i][i+len]<eps||dp[i][j-1]+dp[j+1][i+len]+w[i][i+len]<dp[i][i+len])
                    dp[i][i+len]=dp[i][j-1]+dp[j+1][i+len]+w[i][i+len],root[i][i+len]=j;
    cout<<"最优二叉搜索树的期望代价是 : "<<dp[1][n]<<endl;
    dfs(1,n);   //输出树的结构
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m0_51864047

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值