📄 jiafen.c
字号:
/*
使用算法:动态规划
算法运行时间:O(n^3)
题目补充点二:如果有一颗二叉树形如图1,那么它的中序遍历为bac,前序遍历为abc,后序遍历为bca。
a
/ \
b c
(图1)
题目补充点三:输入样例所对应的二叉树如图2,请读者写出图2的中序遍历与前序遍历,算出得分,并与题目对照是否正确,确保读懂题意!
3
/ \
1 4
\ \
2 5
(图2)
题目补充点四:任何分数均不会出现负值。
明确上面几点后现在我们来考虑如何做这道题,首先要看到一点:题目给出的是中序遍历,并且遍历号为1、2、3、4、……、n,那么如果i是树的根,则所有编号小于i的节点在树中的位置均位于i的左边,所有编号大于i的节点均位于i的右边。
根据这点我们来考虑更为一般性的定理。定理一:如果有一颗子树,他的中序遍历为a1、a2、a3、……、an(“a”后面的字符看成下标,如出现ai-1时,下标是i-1,表示第i-1个数,而不是第i个数减去1,下同),且子树的根为ai,那么a1、a2、a3、……、ai-1均位于ai的左边,ai+1、ai+2、……、an均位于ai的右边。
根据定理一,题目给出一个中序遍历为1、2、3、……、n(这里的数字表示节点的编号,后面提到节点i及表示编号为i的节点)的二叉树时我们可以任意假定某个节点i为树的根而不会打乱二叉树的中序遍历,因为我们只要在二叉树中把编号小于i的节点都放在i的左边,编号大于i的节点都放在i的右边即可,而凑巧的是,在中序遍历序列中也有这一性质(及在中序遍历序列中i左边的数均小于i,i右边的数均大于i),这为我们使用动态规划方法来解此题提供了可能性。
更进一步考虑,如果我们假设节点i为整个树的根,那么节点1、2、3、……、i-1就组成了根节点的左子树,同样节点i+1、i+2、……、n组成了根节点的右子树。此时我们又可以假设在左子树中节点j(j<i)是左子树的根,再次运用定理一可知,这种假设不会打乱整个树的中序遍历。同样对于右子树也成立。
下面我们来考虑动态规划的方法,我们暂时不考虑输出前序遍历的问题(关于动态规划的更多信息请参考《算法导论》第15章):
考虑一:最优子结构。
对于一颗中序遍历为a1、a2、……、an的加分二叉树,我们虽然不知道究竟哪个节点才是得分最大的二叉树的根节点,但是我们可以每一个节点都试一下,看看那个节点作为根节点时能够获得的分数最大。比如当我们考虑把节点ai作为根节点,那么此时摆在我们面前的问题就是左、右子树中的根又是哪个节点呢?答案是:左子树的根节点是作为根时能够让左子树得分最大的节点,右子树的根节点是作为根时能够让右子树得分最大的节点。请读者自己证明!
那么加分二叉树的最优子结构可以表述如下:当选择某节点为根时,如果要获得最大的分数,那么根的左节点必使得左子树能够获得最大分数,同样根的右节点必使得右子树能够获得最大分数。
考虑二:重叠子问题。
根据上面考虑的最优子结构性质,对于给出一个二叉树的中叙遍历求最大得分问题,我们需要考虑的仅有三个小问题,第一是如何选择一个节点作为树的根使得得分最大,第二是如何在左子树中选择一个节点作为左子树的根使得左子树的得分最大,第三个问题是如何在右子树中选择一个节点作为右子树的根使得右子树的得分最大。很明显,问题二和问题三是问题一的重叠子问题。根据重叠性质可知,如果我们用函数MaxTreeScore(left,right)表示一个中序遍历为left、left+1、left+2、……、right的二叉树所能得到的最大加分,用变量nodeScore[i]表示编号为i的节点的分数,那么就有公式
MaxTreeScore(left,right) = max{ MaxTreeScore(left,middle-1) * MaxTreeScore(middle+1,right) + nodeScore[middle] },其中minddle从left递增循环到right。这一点请读者自己证明!
考虑三:子问题独立。
根据上面的分析,子问题便是问题二和问题三,所谓子问题独立就是说问题二的解决不会影响到问题三的解决,这一点显然成立,请读者自己证明!
考虑四:做备忘录。
在程序运行中可能多次使用相同的参数调用MaxTreeScore函数,比如某时刻调用了MaxTreeScore(3,21),另一时刻再次调用MaxTreeScore(3,21),很明显两次调用得到的结果是相同的,因此可以用一个全局变量来保存这个值,当第一次调用时算出函数值保存起来,第二次调用时直接获得此值而不用再次计算,这样可以节约大量的时间!程序中我们用maxTreeScore[left][right]来保存MaxTreeScore(left,right)的值,注意,开头为大写字母的是函数,开头为小写字母的是变量,在我写的代码中均使用此规则!
考虑五:边界。
MaxTreeScore函数会调用自己,但也不能每一次都调用自己,要不就会出现死循环调用,那么什么时候才是个头呢?这就是边界,跟据题意“叶子的加分就是叶节点本身的分数”可知当代如MaxTreeScore函数的两个参数相等时就是求叶节点加分的情况,即MaxTreeScore(x,x)就为x节点的分数nodeScore[x],根据题意“若某个子树为空,规定其加分为1”可知,当代如MaxTreeScore函数的第一个参数大于第二个参数时此树为空(不存在),即MaxTreeScore等于1。
考虑六:所求。
因为我们要求的是整个树能够得到的最大分数,根据上面的分析可知题目要求我们求出的最大分数即为MaxTreeScore(1,n)。注意,如果我们从0开始给节点编号的话所求就相应地为MaxTreeScore(0,n-1),并且程序中就是这样做的!
最后,我们来考虑如何输出一个分数获得最大的二叉树的前序遍历,遍历二叉树需要知道些什么?遍历二叉树需要知道些什么?遍历二叉树需要知道些什么?请读者先想一想!
只要给我们任意一个子树(包括整棵树)的中序遍历我们都知道这个子树的跟节点是哪个的话,便可以前序遍历整个二叉树!(其实不仅是前序遍历,中序遍历、后序遍历都行!这一点请读者自己证明。)比如我们用Tree{left,right}表示中序遍历为left、left+1、left+2、……、right的二叉树,用root[left][right]保存这颗树获得最大分数时的根节点,那么对Tree{left,right}进行前序遍历时及为先遍历跟节点root[left][right],然后遍历左子树Tree{left,root[left][right]-1},最后遍历右子树Tree{root[left][right]+1,right}。这一点类似于上面考虑的重叠子问题。
参考程序代码如下
*/
#include <cstdlib>
#include "iostream"
#include "fstream"
using namespace std;
const int max_n = 30;//最大节点数
int n;//节点数,
int nodeScore[max_n];//nodeScore[i]表示第i个节点的分数
int maxTreeScore[max_n][max_n];//maxTreeScore[left][right]保存了由节点left、left+1、left+2、……、right构成的子树能够获得的最大分数
int root[max_n][max_n];//root[3][8]=5 表示由节点3、4、5、6、7、8构成的子树在获得最大分数时根节点为5
//初始化全局变量,从输入文件中读取数据
void init()//运行时间:O(n^2)
{
ifstream inputFile("tree.in");
inputFile>>n;
for(int i=0; i<n; i++)
inputFile>>nodeScore[i];
inputFile.close();
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
{
maxTreeScore[i][j] = -1;//初始化为-1,表示这个值为“未知”,因为分数不会出现负值所以此方法有效,否则需要设定一个特殊值表示“未知”
root[i][j] = -1;//同样用-1表示“未知”
}
}
//MaxTreeScore(left,right)表示一个中序遍历为left、left+1、left+2、……、right的二叉树所能得到的最大加分
/**//*
分析这个函数的运行时间,我们需要用到动态规划时间分析工具,如下:
考虑1:子问题数--questionCount
所面对的问题不外乎求MaxTreeScore(i,j)的分数,那么总共要求多少次分数呢?
及C(n,2)次,其中C(m,n)表示m个数中取n个的组合方案数,所以questionCount = O(n^2)
考虑2:每个子问题所面临的选择数--chooseCount
这里的选择也就是对根的选择,明显对于长度为len的子树有O(len)个选择。所以chooseCount = O(n)
因此运行MaxTreeScore(0,n-1)共需时间为:questionCount * chooseCount = O(n^3)
*/
int MaxTreeScore(int left, int right)//运行时间:O( (right - left) ^ 3 )
{
//申明这颗树能够获得的最大分数
int score = -1;
//申明这颗树获得最大分数时的根节点编号,使用后追“LR”是为了避免与全局变量重名
int rootLR = -1;
//如果这颗树的最大分数已经算出
if(maxTreeScore[left][right] != -1)
{
//则从保存最大分数的全局变量中获取
score = maxTreeScore[left][right];
rootLR = root[left][right];
}
//如果否则这颗树的最大分数还没有算出,且这颗树为空
else if(left > right)
{
//那么分数为1
score = 1;
}
//否则如果这颗树的最大分数还没有算出,且这颗树是个叶节点
else if(left == right)
{
//那么树的分数为节点分数
score = nodeScore[left];
//根节点为叶节点自身
rootLR = left;
}
//否则这颗树的最大分数还没有算出,且这颗树必还有子结点
else
{
//那么使用公式计算最大分数
for(int i=left; i<=right; i++)
{
//如果i节点作为根节点能够获得更大的分数
if(score < MaxTreeScore(left,i-1) * MaxTreeScore(i+1,right) + nodeScore[i] )
{
//更新最大分数
score = MaxTreeScore(left,i-1) * MaxTreeScore(i+1,right) + nodeScore[i];
//更新根节点为i
rootLR = i;
}
}
}
//保存下这个最大分数避免再次计算
maxTreeScore[left][right] = score;
//保存下获得最大分数时的根节点编号,以便输出前序遍历序列
root[left][right] = rootLR;
//返回最大分数
return score;
}
//输出子树Tree{left,right}的前序遍历
/**//*
分析此函数的时间需要用到“分治法时间分析工具”,了解这个工具请参考我写的其它文章,分析如下
T(len) 表示len= right - left 时所需时间
a = 2;
b = 2;
f(len) = O(1);
考虑情况1,当s = 1(s>0)时,O(len^(log_b_a - s)=O(len^(log_2_2 - 1))=O(1)=f(len)
得运行时间为:T(len) = O(len^(log_2_2)) = O(len)
*/
void ShowPreorderTraversal(int left, int right)//运行时间:O(right - left)
{
//如果树存在
if(left <= right)
{
//先输出根节点,别忘了我们是从0开始编号,所以输出时要增加1
cout<<root[left][right] + 1<<" ";
//再输出左子树的前序遍历
ShowPreorderTraversal(left,root[left][right]-1);
//最后输出右子树的前序遍历
ShowPreorderTraversal(root[left][right]+1,right);
}
}
//输出结果
void Show()//运行时间:O(n^3)
{
//输出最大分值
cout<<MaxTreeScore(0,n-1)<<endl;//运行时间:O(n^3)
//输出整个树的前序遍历
ShowPreorderTraversal(0,n-1);//运行时间:O(n)
}
int main(int argc, char *argv[])
{
//初始化
init();
//显示结果
Show();
system("PAUSE");
return EXIT_SUCCESS;
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -