动态规划4-最优二叉查找树

转自:http://blog.csdn.net/mengzhejin/article/details/37880641

前面说过动态规划最典型的就是解决最优化问题的(具有最优子结构的最优化问题),最优二叉查找树就是一个典型的最优化问题。

问题描述:

给定一个n元素的中序序列,它可以有卡特兰数个不同形状的二叉排序树。(卡特兰数的定义及证明参见组合数学):

《动态规划4-最优二叉查找树》

,如果我们知道每个键的查找概率,怎么来构造一个平均查找代价最小(查找成功)的最优二叉查找树呢?

————————————————————————————————————-

用动态规划来求解,首先要找到它的最优子结构性质,然后根据这个最优子结构来描述和刻画问题,得到状态转移的方程

1)最优子结构性质:

看看一颗最优二叉查找树是怎么得到的?逆向思维,如果现在有一棵最优二叉查找树,root是ak,很容易得出:ak的左右子

树也是最优二叉查找树(如果它的子树不是最优的,那就说明这个子树还可以继续调整,那么ak那颗树就也不是最优的

了)。

《动态规划4-最优二叉查找树》

2)根据最优子结构性质来描述和刻画问题

C[i , j]表示从 i 到 j 的最优二叉查找树的代价,那么问题就被划分为了n^2个子问题了(顶点号从0计数),假设有n个顶

点,那么我们的目标是要求C[0 , n-1]。(编号从0还是1开始无所谓,在编程的时候注意下标范围就行了)。

现在根据它的最优子结构来找状态转移方程:

从 i 到 j的一个最优二叉查找树是怎么得到的?(即一个C[i , j]是怎么来的),它是从 i 到 j 之间的顶点中选出一个顶点来做

root,假设选出的这个做root的顶点是 k (i <= k <= j ),那么显然有:

《动态规划4-最优二叉查找树》

这个式子其实可以直接想到,不用那么复杂的推导,它就是要找一个能使得C[i , j]代价最小的 k (这个k的范围在 i 到 j之

间),而后面为什么要加一个从i到j的概率呢?因为挑出了k后,它作root,每个点的查找长度都增加了1。当然,也有更严格

的推导,可以参考下:

《动态规划4-最优二叉查找树》

3)有了状态转移方程,就可以画个矩阵看看初始条件,以及每个C[i , j]依赖那些值(填表顺序)。

初始条件有:C[i , i] = Pi,C[i , i-1] = 0

试探一下一个C[i , j]是怎么来的,就可以看出,应该沿对角线来填

注意状态转移方程里当 k = i 或者 k = j 时,C[i , i – 1] 或者 C[j+1 , j]是没有定义的,在编程中只需要特殊处理下就

行:对于这种没有定义的取0,其他的取矩阵中的值。

最后一点,至于具体的实现,tmd书上总喜欢画一个不是从0开始的表,有时候甚至还横坐标从0开始,纵坐标从1开始,虽说

是为了填矩阵的方便,但看起来很狗。我一般n规模的问题,就开n * n的矩阵,下表从0到n-1,对超出边界的做一些特殊处

理就行了,就像上面的C[i , i-1]。看看书上的表(理解意思,具体实现我开的矩阵不一样,下标控制不一样):

《动态规划4-最优二叉查找树》

它这样来画表其实就是为了解决C[i , i-1]不在定义范围内,为了能直接从矩阵中取值才这么做的。

————————————————————————————————————-

上面就构造出了最优二叉查找树的最优代价的动态规划过程,利用上述状态转移方程可以填出所有的C[i , j]。

还有一个问题,跟上一篇文章中提到的一样,怎么去不仅仅得到C[i , j]这个代价,更要知道对应于这个代价的二叉树的形

状?

仍然是构造一个矩阵 A[0…n-1,0…n-1] 来记录动态规划的过程,每次选出一个 k 作root时,就把 k 记录下来,即用

A[i , j] = k 表示从 i 到 j 的最优二叉查找树的root是 k。(它还蕴含从 i 到 k – 1是左子树,k+1到 j 是右子树,注意我们

给定的从0到n-1顶点是一个中序序列!)

初始值 A[i , i] = i,表示只有自己的最优二叉查找树的root就是它自己。最后将得到一个矩阵A。它表达了二叉查找树的形

状,当然,还得根据A的含义,从A中获取从 i 到 j的最优二叉查找树的形状。

《动态规划4-最优二叉查找树》

可以有下列算法,从A中输出从 i 到 j 的最优二叉查找树的形状(输出它的前序序列,因为中序序列是已知的):

已知前序序列和中序序列,一个二叉树的形状就确定了:

《动态规划4-最优二叉查找树》

也是用递归(最优子结构),其实方法跟上一篇Floyd的也差不多。

————————————————————————————————————-

实现:

《动态规划4-最优二叉查找树》

package
Section8;


/*
第八章 动态规划 最优二叉查找树(难!!!)
*/


public

class
OptBST {


/**

*

@param
args

*/


public

static

void
main(String[] args) {

//
TODO Auto-generated method stub



float
[] P
=
{(
float
)
0.1
,(
float
)
0.2
,(
float
)
0.4
,(
float
)
0.3
};


//
若返回值是最小代价,测试最小代价是否正确

//
System.out.println(“输出最优二叉排序树的最小代价:\n”);

//
float result = OptBST(P);

//
System.out.println(result);


//
若返回值是表达最优二叉排序树形状的矩阵,测试矩阵是否正确


System.out.println(

输出表达最优二叉排序树形状的矩阵:\n

);

int
[][] R
=
OptBST(P);

for
(
int
i
=

0
;i
<
R.length;i
++
)
{

for
(
int
j
=

0
;j
<
R.length;j
++
)
System.out.print(R[i][j]

+


  

);
System.out.println();
}

}


public

static

int
[][] OptBST(
float
[] P){

//
接受一个中序序列的点的查找概率数组,返回最优的二叉查找树的代价(注意P中的概率按顺序对应于点的中序序列)



int
n
=
P.length;
//
结点个数



float
[][] result
=

new

float
[n][n];


int
[][] R
=

new

int
[n][n];
//
表达二叉查找树形状的矩阵




for
(
int
i
=

0
;i
<
n;i
++
)
{
result[i][i]

=
P[i];
//
填充主对角线C[i,i] = P[i]


R[i][i]
=
i;
//
R[i][j]表示若只构造从i到j的树,那么root是R[i][j]


}


for
(
int
d
=

1
;d
<=
n


1
;d
++
)
//
共n-1条对角线需要填充


{

for
(
int
i
=

0
;i
<=
n

d


1
;i
++
)
//
横坐标的范围与对角线编号d的关系


{

int
j
=
i
+
d;
//
一旦横坐标确定后,纵坐标可以用横坐标与对角线编号表示出来



float
min
=

1000000
;


int
root
=

0
;


for
(
int
k
=
i;k
<=
j;k
++
)
{

float
C1
=

0
,C2
=

0
;
//
用C1,C2表示result[i,k-1]和result[k+1,j]



if
(k
>
i)
C1

=
result[i][k


1
];

if
(k
<
j)
C2

=
result[k
+

1
][j];


if
(C1
+
C2
<
min)
{
min

=
C1
+
C2;
root

=
k;
}
}

R[i][j]
=
root;
//
R[i][j]的值代表从i到j的最优二叉查找树的根




float
sum
=

0
;

for
(
int
s
=
i;s
<=
j;s
++
)

//
sum = sum + P[i];
//
你妈啊,一个小错误找了半天


sum
=
sum
+
P[s];

result[i][j]
=
sum
+
min;
}
}


//
return result[0][n-1];
//
返回C[1,n],最小代价



return
R;
//
返回表达最优二叉排序树形状的矩阵


}

}
《动态规划4-最优二叉查找树》

最优代价的矩阵和表达形状的矩阵在一起求的,需要哪个就返回哪个值,见代码。

很容易看出
时间复杂度是 n^3(k的选择需要一个循环) 的,空间复杂度是 n^2的

运行结果(返回表达二叉查找树形状的矩阵R):

输出表达最优二叉排序树形状的矩阵:

0  1  2  2  

0  1  2  2  

0  0  2  2  

0  0  0  3  

————————————————————————————————————-

思考:

1,分析最优二叉查找树的时间复杂度,空间复杂度。(见分析讲解)

2,写一个线性时间的伪代码,从根表(矩阵A)生成最优二叉查找树(见分析讲解)

3,判断正误:一颗最优二叉查找树的根总是包含概率最高的键(错,很容易举反例,如果这句话是对的,那还要动态规划干

吗,那就可以用更简单的办法来构造最优二叉排序树了–直接根据概率选最大值就行了)

4,把最优二叉查找树(查找成功的代价最小)推广到成功和不成功总代价最小的情况(easy,改变C[i ,j]的含义,加上不成

功的概率即可)

5,证明:

《动态规划4-最优二叉查找树》

重要:事实上
卡特兰数就是这样定义的
,(如果后面有机会,我会深入学习下卡特兰数),该递推关系的解是由卡特兰数给

出的(现在都忘了,可以参见《组合数学》  卢开澄   北京大学出版社)。

如果不要求严格的证明,根据b(n)的含义,其实很容易写出上述递推式。(想一想)

————————————————————————————————————-

最后一题:矩阵连乘问题,见下一篇文章,作为一个解题报告。

————————————————————————————————————

总结:

1)怎么去分析一个问题,试图用动态规划去解决?(
先找最优子结构,从最有子结构去刻画和描述问题

2)
逆向思维去找状态转移方程

3)相对前面的几个动态规划算法,最优二叉查找树的填矩阵顺序稍显复杂(沿对角线),直接看递推式的数学形式可能看不

出来,画出矩阵,去尝试一个C[i ,j]是怎么求出来的,就能清晰的看出来。

4)卡特兰数非常重要,许多问题都是卡特兰数问题(参见什么叫做professional?),习题5给出了一个很好的引导证明。

    原文作者:二叉查找树
    原文地址: https://blog.csdn.net/ltx06/article/details/42081961
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞