最优二叉搜索树
问题描述
二叉搜索树我们都知道,左子树结点的值都小于根结点,右子树结点的值都大于根节点。如果某个结点没有左子树或右子树,那么在对应的位置上加一个虚结点。现在,给出n个结点的搜索概率
p
i
p_i
pi,以及n+1个虚结点的搜索概率
q
i
q_i
qi(这些结点的值按递增排列),问最优二叉搜索树的搜索代价,以及这棵树的具体构造。
其中,实结点的搜索代价为它的深度+1,虚结点的搜索代价为它的深度。
问题分析
- 什么是虚结点,为什么要考虑虚结点?
给定一棵二叉搜索树,如下图。除了对树中存在的结点的查询需要代价,对一些不存在的点也需要经过判断才能得出它“不在树中”的结果。例如,在下面棵树中,我们查询6这个不存在结点,需要将指针从根节点7移动两次到达结点5,这时才能得出结论,6不存在。所以把这些不存在的点看作是一个虚结点。
- 如何计算最优二叉树
我们不能交换树中的两个结点,因为它是一棵“二叉搜索树”,左子树必定小于根节点,右子树必定大于根节点,对结点的交换会破坏这一性质。我们可以做的是,在一个区间内,选择一个结点来成为根节点。例如,现在有个实结点序列,它的查找概率存储在数组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][k−1]+w[i][k−1]∗1(因为区间[i][k-1]成为了结点k的左子树,整体深度+1,那么搜索代价就会增加 w [ i ] [ k − 1 ] ∗ 1 w[i][k-1]*1 w[i][k−1]∗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][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]
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][k−1]+dp[k+1][j]+w[i][j])
那么,如何得知所有子问题的答案呢?
我们发现,子问题为 d p [ i ] [ k − 1 ] dp[i][k-1] dp[i][k−1]与 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[k−1]+q[k+1]
其中 p [ k ] p[k] p[k]代表实结点k的搜索概率, q [ k − 1 ] q[k-1] q[k−1]、 q [ k + 1 ] q[k+1] q[k+1]代表虚结点k-1与k+1的搜索概率。
所以,最小的子问题可以由输入计算出来,一步步由小问题的答案,推出大问题的答案,就可以得出结果。 - 实现方法
前面的分析是从上往下的,实际编程时,我们要预处理出所有的 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); //输出树的结构
}