AcWing 479. 加分二叉树
. 加分二叉树
一、题目描述
设一个 个节点的二叉树 的 中序遍历 为,其中数字 为节点编号。
每个节点都有一个分数(均为正整数),记第 个节点的分数为 , 及它的每个子树都有一个加分,任一棵子树 (也包含 本身)的加分计算方法如下:
的左子树的加分 的右子树的加分 的根的分数
若某个子树为空,规定其加分为 。
叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为且加分最高的二叉树 。
要求输出:
(1)的最高加分
(2)的前序遍历
输入格式
第 行:一个整数 ,为节点个数。
第 行: 个用空格隔开的整数,为每个节点的分数(<分数<)。
输出格式
第 行:一个整数,为最高加分(结果不会超过范围)。
第 行: 个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。
数据范围
输入样例:
5
5 7 1 2 10
输出样例:
145
3 1 2 4 5
三、解题思路
前导知识:二叉树的三种遍历方式
题目的输入为这个点 中序序列 的权值,我们直接存储。这里科普一下,一个二叉树的中序序列就是这个二叉树的节点 向下投影 构成的序列。
而事实上仅靠投影无法断定这个二叉树的具体结构,因此前序遍历的序列也会不同。
比如1 2 3 4 5
这个中序序列
它可以是
3
2 4
1 5
也可以是
3
1 4
2 5
当然也可以是
4
2 5
1 3
在固定的中序序列条件下,枚举不同的根节点和叶子节点构建形态,可以获取不同的结构,也就是形态各异的二叉树,它们的前序遍历是不一样的。
解释:
- 心中有树,而手中无树
虽然给的是二叉树,但却无法用链式前向星把二叉树创建出来,因为这不是唯一的二叉树,不是一道图论题。
闫式分析法
状态表示
集合
:从到区间内,选一点作为根节点,表示中序遍历是 的所有二叉树的集合
属性
加分二叉树的最大值
状态转移
任选节点,以点为根构成的加分二叉树的值=它的左子树的最大加分值 右子树的最大加分值 点权值
:石子合并枚举的满足条件为,为什么本题是呢?
答:
-
石子合并至少需要两堆才能合并,即有,也必须有,才能划分成两个区间。
换句话说,就是,也就是。 -
回到本题,因为左子树右子树可以为空,也就是如果时,左子树为空,如果时,右子树为空, 题目中明确:若某个子树为空,规定其加分为 ,也就是此时就可以得到正确的得分了。
前序序列
我们只需要获取一个的时候,同时开一个相同大小的数组,存储此区间内的根节点是谁,其实这是一个递归定义,我们捋着这条线索,就可以一路向上(或向下),找出完整的二叉树形态,也就是在中序序列确定情况下的前序序列,捋的办法就是用递归。
:如果存在多种方案,则输出字典序最小的方案。
答: 因为是从小到大遍历的,所以肯定是小的先存进去。而对比时采用了小于号,只有更小的才能更新,就保证取得的节点是字典序最小的方案。
另外一种就是不用记录数组,就是现推,现看是从哪个值转移过来的,边计算边退,两个都是一样的。
四、代码
#include <bits/stdc++.h>
using namespace std;
const int N = 50;
int n;
int w[N]; // 权值数组
int f[N][N]; // DP数组 : i,j区间内的最大得分
int g[N][N]; // 路径数组:i,j区间内的最大得分,是在k这个节点为根的情况下获得的
// 前序遍历输出路径
void out(int l, int r) {
/*
其实,最后一个可以执行的条件是l=r,也就是区间内只有一个节点
当只有一个节点时,g[l][r] = l = r ①,此时继续按划分逻辑划分的话:
就是: [l,g[l][r]-1],[g[l][r]+1,r] ②
将①代入②,就是[l,l-1],[r+1,r],很明显,当出现前面比后面还大时,递归应该结束了
*/
if (l > r) return;
cout << g[l][r] << " "; // 输出根结点,前序遍历
out(l, g[l][r] - 1); // 递归左子树
out(g[l][r] + 1, r); // 递归右子树
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
/* DP初始化
① 因为左、右子树为空时,返回1,这其实也是为计算公式准备前提,按返回1计算时,无左右子树的点
,也就是叶子节点时,得分就是w[i]
② 此时,记录的辅助路径数组,g[i][i]表示的就是从i到i的区间内,根是谁,当然是i
Q:为什么一定要记录g[i][i]=i,不记录不行吗?一个点的区间为什么也要记录根是谁呢?
答:因为这是递归的出口,如果不记录的话,那么out输出时就会找不到出口,导致死循环,TLE或者MLE
*/
for (int i = 1; i <= n; i++)
f[i][i] = w[i], g[i][i] = i;
// 区间DP
for (int len = 2; len <= n; len++) // 单个节点组成的区间都是初值搞定了,我们只讨论两个长度的区间
for (int l = 1; l + len - 1 <= n; l++) { // 枚举左端点
int r = l + len - 1; // 计算右端点
// 枚举根节点k, 两个区间:[l,k-1],[k+1,r],根节点k也占了一个位置
// 注意:k是可以取到r的,原因论述见题解
for (int k = l; k <= r; k++) {
// 根据题意特判
int ls = k == l ? 1 : f[l][k - 1]; // 左子树为空,返回1
int rs = k == r ? 1 : f[k + 1][r]; // 右子树为空,返回1
// 得分计算公式
int t = ls * rs + w[k];
// 记录取得最大值时的根节点k
if (f[l][r] < t) {
f[l][r] = t;
g[l][r] = k; // 记录l~r区间的最大得分是由哪个根节点k转化而来
}
}
}
// 输出
cout << f[1][n] << endl;
// 利用递归,输出字典序路径
out(1, n);
return 0;
}
五、记忆化搜索
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int n;
int w[N];
int f[N][N];
int g[N][N];
// 计算最大结果
int dfs(int l, int r) {
int &v = f[l][r]; // 简化代码
if (v) return v; // 记忆化搜索
if (l == r) return g[l][r] = l, v = w[l]; // 叶子分数是权值
if (l > r) return v = 1; // 题设空子树的分数为1,递归出口
for (int k = l; k <= r; k++) {
// 因为k是枚举根,根占了一个点,所以,左侧是[l,k-1],右侧是[k+1,r]
int t = dfs(l, k - 1) * dfs(k + 1, r) + w[k];
if (t > v) v = t, g[l][r] = k; // 记录第一次出现最大值时的k,方便输出字典序
}
return v;
}
// 前序遍历输出路径
void out(int l, int r) {
if (l > r) return; // 递归出口
cout << g[l][r] << " "; // 输出最优根
out(l, g[l][r] - 1); // 递归左子树
out(g[l][r] + 1, r); // 递归右子树
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
// 区间1~n的最大结果
cout << dfs(1, n) << endl;
// 输出路径
out(1, n);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2015-01-07 高效访问Internet-启用ISA Server的缓存