码迷,mamicode.com
首页 > 编程语言 > 详细

【算法导论】动态规划之“最优二叉搜索树”

时间:2015-01-24 17:26:03      阅读:326      评论:0      收藏:0      [点我收藏+]

标签:算法导论   最优二叉搜索树   钢管切割   矩阵链乘法   动态规划   

        之前两篇分别讲了动态规划的“钢管切割”“矩阵链乘法”,感觉到了这一篇,也可以算是收官之作了。其实根据前两篇,到这里,也可以进行一些总结的,我们可以找到一些规律性的东西。


        所谓动态规划,其实就是解决递归调用中,可能出现重复计算子问题,从而导致耗费大量时间,去做重复劳动的问题。解决思路就是,将重复做过的子问题的结果,先存起来,等之后再需要用到的时候,直接拿过来用,而不需要再去计算。


        但是这里还需要注意一些地方:

        

        ①要解决的问题,比如“钢管切割”中的长钢管,它的最优化的解,要能够是分解成的子问题的最优化的解的组合。

        ②如何给出子问题,孙子问题等的最优化解的统一递归调用形式。

        ③找一个或者几个合适的集合,去保存我们求的子问题的解,或者是划分的方案。比如在钢管切割中,我们使用的一位数组,而到了矩阵链乘法中,我们使用的二维数组。

        ④采用自顶向下还是自底向上的方案。



一、概述


        好,言归正传,来看看最优二叉搜索树。关于二叉搜索树,可以参看之前的两篇文章:二叉树的前中后序非递归遍历实现二叉搜索树的插入和删除。这里不再赘述。


        假如说我们要设计一个程序,来实现英语文本到法语的翻译。我们可以建立一个二叉搜索树,将n个英语残次作为关键字,对应的法语单词作为关联数据。然后我们依次遍历二叉搜索树中每个节点的英文单词,来找到合适的单词进行翻译工作。这里我们肯定是希望这个搜索的时间越少越好,如果只考虑单纯的时间复杂度,我们可以考虑采用红黑树等,来达到搜索时间复杂度为Olg(n)。但是对于英语单词而言,每个单词出现的概率是不一样的,比如"the"等单词。显然的让这类单词越靠近根结点越能减少搜索的时间。而对于某些很少见的单词,则可以让它们远离根结点。


        问题来了,假如我们已知n个不同关键字ki以及我们的出现概率p[i],我们如何来组成这样一颗最优二叉搜索树呢?

        

        查找这棵二叉树的结果不外乎是两种,一种是找到了我们想要的key值,另一种是遍历完整棵树都没有找到我们要的key值,我们将这种没有找到key值也安排一些节点di来表示,那么显然di节点都是叶子节点。称di为伪关键字,对应的概率为q[i]。d0代表所有小于k1的值,dn代表所有大于kn的值,其他的di(1<=i<=n-1),代表di值位于ki和ki+1之间。因为一次搜索,要么是搜到di,要么是搜到一个ki,所有有:

                                                                           技术分享

        然后现在我们可以给出搜索一次会搜索到的节点数的一个期望,因为对于二叉树而言,每多一层,则就会多查找一个节点,所以我们可以对每个节点的深度加1,然后乘以该节点的pi,然后将所有节点的结果求和,就可以求出搜索一棵树会搜索到的节点的一个期望:

                                                                                                                       技术分享


        最优二叉搜索树的定义就是:对于给定的节点key值概率,使得上述期望值最小的二叉搜索树结构。



二、使用动态规划来处理最优二叉搜索树


        那么还是那几步:


        ①首先将问题分成子问题,看看问题的最优是不是子问题也最优的时候可以达成。

        

        可以使用“jianqie -粘贴”法来证明,对于一颗最优二叉搜索树T而言,对于T的一颗子树Ti,假设它包含关键字ki....kj,如果Ti不是最优二叉查找树,那么就意味着存在另一棵包含节点ki...kj的二叉搜索树Tj,使得Tj的搜索期望比Ti的搜索期望低,那么我们可以将Ti从T中“剪切”掉,然后将Tj“粘贴”到原来Ti的位置,这样一来,就发现新生成的树肯定要比原来的树T的搜索期望要低了,这是矛盾的,因为我们原来假设T是一棵最优二叉搜索树。所以可以反证,最优二叉搜索树的子树都是最优二叉搜索树。

        于是我们可以确定,最优二叉搜索树问题,可以分解成子问题,而且子问题也是最优二叉搜索树问题,而又可以很容易的遇见,在分成的子问题中,会有多个重复的子问题,孙子问题等,所以满足动态规划的条件,从而可以使用动态规划方法来解决。


        ②其次要解决的是递归子问题的最优化解的统一形式(计算式)

        这时又跟之前的“光管切割”和矩阵链乘法有些相似了,很容易看出,我们要将一棵树的搜索期望问题,分解成:左子树的搜索期望问题+根结点的搜索期望+右子树的搜索期望;对于根结点而言,它的深度为0,那么直接使用(0+1)*p[根的位置],即是搜索期望。而对于左右子树而言,如果它们是一棵单独的树,那么也可以直接使用(左子树的搜索期望问题+根结点的搜索期望+右子树的搜索期望)的形式来递归,但是这里,左右子树比简单的二叉树的情况要多一层深度,因为它们头顶上要多了一个根结点,所以说这里需要将每一个节点都多加上一个它们自身的出现概率p[i]或者q[i](这里包括了关键字节点和伪关键字节点),于是增加量为每棵树所有节点的概率之和。令w(i,j)表示包含节点ki...kj的树,所有节点的概率之和,于是有:

                                                                技术分享

假如我们以r处的节点为根结点,有:

                                                                  技术分享

而根据上面的分析,我们将一棵树的搜索期望,假设根结点在Kr处,分解成根结点的概率p[r]和左子树的搜索期望加左子树的节点概率和:e(i,r-1)+w(i,r-1),右子树的搜索期望加上右子树的节点概率和:e(r+1,j)+w(r+1,j)。


这里还要考虑的一种情况,就是e(i,j),当j=i-1的情况,这时子树不可能包含关键字,因为关键字的序号i<=k<=j,而j=i-1,显然不满足。所以这时子树只会包含一个伪关键字节点:di-1。


于是根据上面两个公式,有:

                                                                                                                    技术分享


这样就求出了我们需要的子问题递归统一形式了


        ③选用合适的数据结构来存储子问题的处理结果和划分信息。

        这里我们需要保存的数据有:每个子树的最佳搜索期望值e(i,j),每个子树的最佳时刻根结点位置r(i,j),和每个子树的概率和的值:w(i,j)。


        ④采用自底向上的方式来解决子问题和父问题。


于是,按照之前“矩阵链乘法”的思路,容易给出下面的代码:

const int MaxVal = 0x7fffffff;

const int n = 5;
//搜索到根节点和虚拟键的概率
double p[n + 1] = { -1, 0.15, 0.1, 0.05, 0.1, 0.2 };
double q[n + 1] = { 0.05, 0.1, 0.05, 0.05, 0.05, 0.1 };

int root[n + 1][n + 1]; //记录根节点
double w[n + 2][n + 2]; //子树概率总和
double e[n + 2][n + 2]; //子树期望代价

/*
 * p是存放关键字节点概率的数组[1,n],q是存放伪关键字节点(叶子节点)出现概率的数组[0,n]
 * n是节点个数
 * e[i,j]是存放包含关键字Ki~Kj的最优二叉书进行一次搜索的期望代价,由于要包括e[n+1,n]的q[n]和e[1,0]q[0],所以范围是[0,n+1]
 * w[i,j]是存放Ki~Kj的概率和,w[i,j]=w[i,r-1]+p[r]+w[r+1,j]
 * root[i,j]是存放包含Ki~Kj的关键字的子树,最优情况下,root节点的下标
 */
void optimal_Bst(double* p, double* q, int n) {
	//首先处理w[i,j]和e[i,j]中i=j+1的情况,这种情况都是q[i-1]
	for (int i = 1; i <= n + 1; i++) {
		w[i][i - 1] = q[i - 1];
		w[i][i - 1] = q[i - 1];
	}

	//然后处理长度L从1到n的循环

	int l = 0; //代表ki~kj的长度
	int j = 0; //代表最后一个元素的下标值j
	int i = 0; //代表起始的一个元素的下标值i

	int r = 0; //代表root节点的下标

	double tmp = 0; //这里要存储e数组元素计算的临时结果,所以也是double类型的

	for (l = 1; l <= n; l++) {

		//以i为外层循环,这里由于长度是L,i最大为n-l+1,而j-i+1=L,j=L+i-1
		for (i = 1; i <= n - l + 1; i++) {
			j = l + i - 1;

			//先初始化e[i][j]和w[i][j]
			e[i][j] = MaxVal;
			w[i][j] = w[i][j - 1] + p[j] + q[j];

			for (r = i; r <= j; r++) {
				//公式:e[i][j] = e[i][r - 1] + e[r + 1][j] + w[i][j];
				tmp = e[i][r - 1] + e[r + 1][j] + w[i][j];
				if (tmp < e[i][j]) {
					e[i][j] = tmp;
					root[i][j] = r;
				}
			}
		}
	}
}


上面算法的时间复杂度为O(n^3),我们可以作一点优化,让其变成O(n^2),这里需要利用到一个结论:

                                                                                                                     技术分享


将optimal_Bst函数变成如下形式,改变对根结点位置r的遍历方式,当然,对于之前也要添加判断条件:


/*
 * 优化版
 */
void optimal_Bst2(double* p, double* q, int n) {
	//首先处理w[i,j]和e[i,j]中i=j+1的情况,这种情况都是q[i-1]
	for (int i = 1; i <= n + 1; i++) {
		w[i][i - 1] = q[i - 1];
		w[i][i - 1] = q[i - 1];
	}

	//然后处理长度L从1到n的循环

	int l = 0; //代表ki~kj的长度
	int j = 0; //代表最后一个元素的下标值j
	int i = 0; //代表起始的一个元素的下标值i

	int r = 0; //代表root节点的下标

	double tmp = 0; //这里要存储e数组元素计算的临时结果,所以也是double类型的

	for (l = 1; l <= n; l++) {

		//以i为外层循环,这里由于长度是L,i最大为n-l+1,而j-i+1=L,j=L+i-1
		for (i = 1; i <= n - l + 1; i++) {
			j = l + i - 1;

			e[i][j] = MaxVal;
			w[i][j] = w[i][j - 1] + p[j] + q[j];

			if (i == j) {
				root[i][j] = i;
				e[i][j] = p[i] + q[i - 1] + q[j];

			} else {
				//先初始化e[i][j]和w[i][j]

				for (r = root[i][j - 1]; r <= root[i + 1][j]; r++) {
					//公式:e[i][j] = e[i][r - 1] + e[r + 1][j] + w[i][j];
					tmp = e[i][r - 1] + e[r + 1][j] + w[i][j];
					if (tmp < e[i][j]) {
						e[i][j] = tmp;
						root[i][j] = r;
					}
				}
			}
		}
	}
}



        经过上面的修改之后,就可以达到O(n^2)的时间复杂度。


        习题15.1-1,打印出上面最优二叉搜索树的每个节点与其父节点的关系的函数:


/*
 * 打印最优二叉搜索树
 */
void print_optimal_Bst(int i, int j, int r, bool root_flag) {
	int root_node = root[i][j];
	if (root_flag) {
		cout << "根结点为:k" << root_node << endl;
		root_flag = false;
		print_optimal_Bst(i, root_node - 1, root_node, root_flag);
		print_optimal_Bst(root_node + 1, j, root_node, root_flag);
		return;
	}

	if (i > j + 1) {
		return;
	} else if (i == j + 1) {
		if (j < r) {
			cout << "d" << j << "是" << "k" << r << "的左孩子" << endl;
		} else {
			cout << "d" << j << "是" << "k" << r << "的右孩子" << endl;
		}
		return; //这里加个return是因为,当循环到i==j+1的时候,已经到头了,不能再递归了,此时不存在合理的root_node了
	} else {
		if (root_node < r) {
			cout << "k" << root_node << "是" << "k" << r << "的左孩子" << endl;
		} else {
			cout << "k" << root_node << "是" << "k" << r << "的右孩子" << endl;
		}
	}
	print_optimal_Bst(i, root_node - 1, root_node, root_flag);
	print_optimal_Bst(root_node + 1, j, root_node, root_flag);
}


输出结果:


                                       技术分享

                                                                                                                      















【算法导论】动态规划之“最优二叉搜索树”

标签:算法导论   最优二叉搜索树   钢管切割   矩阵链乘法   动态规划   

原文地址:http://blog.csdn.net/cyp331203/article/details/43083647

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!