[NOIP2003 提高组] 加分二叉树
题目描述:
设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:
subtree 的左子树的加分 乘 subtree 的右子树的加分 加 subtree 的根的分数。
若某个子树为空,规定其加分为 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 (1,2,3,…,n) 且加分最高的二叉树 tree。要求输出
-
tree 的最高加分。
-
tree 的前序遍历。
输入格式:
第 1 行 1 个整数 n,为节点个数。
第 2 行 n 个用空格隔开的整数,为每个节点的分数
输出格式:
第 1 行 1 个整数,为最高加分(Ans≤4,000,000,000)。
第 2 行 n 个用空格隔开的整数,为该树的前序遍历。
输入输出样例:
输入 #1:
5 5 7 1 2 10
输出 #1:
145 3 1 2 4 5
说明/提示:
数据规模与约定:
对于全部的测试点,保证 1≤n<30,节点的分数是小于 100 的正整数,答案不超过 4×10^9。
思路:
如果你啥树的知识也不会,没关系,先跟我默念一遍:
树 ,是递归定义的
此话怎讲?我们来看这个题的二叉树的积分规则:
树的加分=左子树的加分× 的右子树的加分+根的分数
也就是说:树的加分,是由“左子树的加分、右子树的加分、根的分数”这三部分来决定的,那每一部分的加分又是怎么算的呢?
其中根的分数拿来用就行,而剩下两个,比如左子树的加分,不难发现左子树其实也是一棵树,它的积分规则也是由这三部分决定的
于是,我们又可以按着左子树的左子树和右子树算,就这样一环套一环,这有没有让你联想到什么呢?
没错,就是递归!
那么递归总要有个头,不能一直下去没完了。那到什么时候才算结束呢?很简单,到叶节点的时候就结束啦!(不知道什么是叶节点的小伙伴注意啦:叶节点就是左右子树都空的节点,就像一棵树的叶子不会再往上长分枝了),递归到叶节点,直接返回该节点的加分,不用往下算啦!
到目前为止,怎么递归已经都想好了,那到底用什么算法,再具体点?
对啦,就是万能的深度优先搜索dfs!.
终于想好了思路,接下来终于要编代码啦!但是这是好多小伙伴肯定又犯了愁。正因为树这种数据结构的特殊性,我们应该怎么存储它呢???难道还需要开一个结构体吗?
悄悄告诉你,只需要一个数组就可以啦!
int n,a[40];//a来存储这棵树
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
没错,我没骗你,就这么简单!
那可能各种问题又来了:我这么存储,怎样才能方便地获取每棵子树的信息呢?
别慌别慌,我们再看一下题目:输入的是这棵树的中序遍历,也就是左子树→根节点→右子树的遍历,我们可以设一个区间l~r,表示这棵树是a[l]到a[r]的部分,来看它是哪棵子树
for example,这棵树的中序遍历是:
5 7 1 2 10
那么1~5就觉得是整棵树,咱们假设中间的1为根节点,那根据中序遍历的顺序,根节点在中间,1左边的部分(5和7)也就是1~2,是左子树,右边的部分(2和10)也就是4~5,是右子树
也就是说,当整棵树为l~r,根结点为i时,左子树为l~i-1,右子树为i-1~r
懂了吧,那么再来几个!2~2是哪?
是叶节点7!
那么5~4呢?
这个是一个空节点,并且,它的加分是1(题目都写了)
综上所述,我们总结到了三种情况:
- l<r时,1~r是指a[l]到a[r]的子树,加分为左子树的加分× 的右子树的加分+根的分数
- l==r时,l~r是指叶节点a[l](或a[r],反正l==r嘛),加分为a[l]的值
- l>r时,l~r为空节点,加分为1
于是我们就可以按这三条原则写出代码啦!在代码里,我们让每一个节点都当一次根节点,看看谁的最大!
long long dfs(int l,int r){ //dfs函数,数据比较大,开long long保险
if(l>r)return 1; //特殊情况1:如果为空节点,返回1
if(l==r)return a[l]; //特殊情况2:如果为叶节点,直接返回该节点的加分
long long maxn=0; //maxn来记录最大加分,作为最后的返回值
for(int i=l;i<=r;i++){
long long t=dfs(l,i-1)*dfs(i+1,r)+a[i];//t为以i为根节点的最大的加分
if(maxn<t) //更新最大值
maxn=t;
}
return maxn;//返回最大值
}
这样我们求最大加分的任务完成啦!但这还不够,这代码要递归这么多次,计算机这么可爱,怎么可以如此虐待计算机!重点是还会TLE,怎么办呢?
我们发现,我们在执行递归的过程中,同一棵子树算了一次又一次,会有很多重复的计算,何不把这些结果存起来,下次再用的时候直接拿过来就成! 好的,我们把代码改一下,由于加分是由l和r两个数决定的,我们就开一个名为dp的二维数组存吧!这次的代码,我们用dp[l][r]来记录l~r子树的最大加分,取代了maxn的位置
long long dp[40][40]={0};//dp[l][r]记录l~r子树的最大加分
long long dfs(int l,int r){
if(l>r)return 1;
if(l==r){
root[l][r]=l;
return a[l];
}
if(dp[l][r])return dp[l][r];//特殊情况3:如果以前已经算过了,那就直接返回以前存起来的结果
for(int i=l;i<=r;i++){
long long t=dfs(l,i-1)*dfs(i+1,r)+a[i];
if(dp[l][r]<t)
dp[l][r]=t;
}
return dp[l][r];//返回最大值
}
刚才的“如果以前已经算过了,那就直接返回以前存起来的结果”的思想,就是传说中的“记忆化搜索”,可以减少许多不必要的计算,再也不用担心我会TLE啦!
接下来的任务,就是输出最大加分的树的先序遍历。先序遍历的顺序是根节点→左子树→右子树,咱们还是用递归解决,定义一个名为root的二维数组,用来存储使l~r子树的加分最大的那个根节点,dfs咱都会编了,这种事那简直是a piece of cake!(小菜一碟)
void print(int l,int r){ //输出这棵树的先序遍历
if(l>r)return; //如果节点为空(依然是l<r)结束
cout<<root[l][r]<<" "; //先序遍历,先输出根节点
print(l,root[l][r]-1); //然后左子树
print(root[l][r]+1,r); //最后右子树
}
(注意题目让你输出的是节点的编号,也就是循环中的i而不是a[i],先把dfs函数里面加入储存root节点的代码,然后在print函数中按照先序遍历的顺序输出就完美解决了)
最后的最后,就是完整代码了!主程序应该很好编了吧!这里给大家介绍另一种处理叶节点的方法:由于它在dp数组和root数组中的值已经确定了,可以将它在主程序的循环中直接初始化最大加分和根结点位置。
完整代码:
#include<iostream>
using namespace std;
int n,a[40],root[40][40];//a来存储中序遍历,root来存储最大积分的根节点
long long dp[40][40]={0};//dp[l][r]记录从l区域到r区域最大的加分
long long dfs(int l,int r){ //电风扇函数
if(l>r)return 1;
if(l==r){ //如果为叶节点,最大积分的根节点就是当前节点l
root[l][r]=l;
return a[l];
}
if(dp[l][r])return dp[l][r];
for(int i=l;i<=r;i++){
long long t=dfs(l,i-1)*dfs(i+1,r)+a[i];
if(dp[l][r]<t){ //更新最大值以及根节点位置
dp[l][r]=t;
root[l][r]=i;
}
}
return dp[l][r];
}
void print(int l,int r){ //输出这棵树的先序遍历
if(l>r)return; //如果节点为空(依然是l<r)结束
cout<<root[l][r]<<" "; //先序遍历,先输出根节点
print(l,root[l][r]-1); //然后左子树
print(root[l][r]+1,r); //最后右子树
}
int main(){
cin>>n;//输入
for(int i=1;i<=n;i++){
cin>>a[i];
dp[i][i]=a[i];//叶节点的最大积分就是当前节点的积分,最大积分的根节点也是当前节点,直接给它初始化
root[i][i]=i;
}
cout<<dfs(1,n)<<endl;//从头到尾搜索
print(1,n);
return 0; //树,是递归定义的
}
总结:
如果该题你认真思考的话,熟知树的结构,那是有很大几率可以做出来的。
祝大家早日AC!
The End~
题目链接:
[NOIP2003 提高组] 加分二叉树 - 洛谷https://www.luogu.com.cn/problem/P1040