AcWing 479. 加分二叉树

\(AcWing\) \(479\). 加分二叉树

一、题目描述

设一个 \(n\) 个节点的二叉树 \(tree\)中序遍历\((1,2,3,…,n)\),其中数字 \(1,2,3,…,n\) 为节点编号。

每个节点都有一个分数(均为正整数),记第 \(i\) 个节点的分数为 \(d_i\)\(tree\) 及它的每个子树都有一个加分,任一棵子树 \(subtree\)(也包含 \(tree\) 本身)的加分计算方法如下:

\(subtree\)的左子树的加分 \(×\) \(subtree\)的右子树的加分 \(+\) \(subtree\)的根的分数

若某个子树为空,规定其加分为 \(1\)

叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为\((1,2,3,…,n)\)且加分最高的二叉树 \(tree\)

要求输出: 
(1)\(tree\)的最高加分 
(2)\(tree\)的前序遍历

输入格式
\(1\) 行:一个整数 \(n\),为节点个数。

\(2\) 行:\(n\) 个用空格隔开的整数,为每个节点的分数(\(0\)<分数<\(100\))。

输出格式
\(1\) 行:一个整数,为最高加分(结果不会超过\(int\)范围)。

\(2\) 行:\(n\) 个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。

数据范围
\(n<30\)

输入样例

5
5 7 1 2 10

输出样例

145
3 1 2 4 5

三、解题思路

前导知识:二叉树的三种遍历方式

题目的输入为这\(n\)个点 中序序列 的权值,我们直接存储。这里科普一下,一个二叉树的中序序列就是这个二叉树的节点 向下投影 构成的序列。

而事实上仅靠投影无法断定这个二叉树的具体结构,因此前序遍历的序列也会不同。

比如1 2 3 4 5这个中序序列

它可以是

        3
    2       4
1               5

也可以是

           3
    1           4
        2             5

当然也可以是

            4
    2            5
1       3

在固定的中序序列条件下,枚举不同的根节点和叶子节点构建形态,可以获取不同的结构,也就是形态各异的二叉树,它们的前序遍历是不一样的。

解释:

  • 心中有树,而手中无树
    虽然给的是二叉树,但却无法用链式前向星把二叉树创建出来,因为这不是唯一的二叉树,不是一道图论题。

闫式\(DP\)分析法

状态表示

集合
\(f[i][j]\):从\(i\)\(j\)区间内,选一点\(k\)作为根节点,表示中序遍历是 \(w[i \sim j]\) 的所有二叉树的集合

属性
加分二叉树的\(Max\)最大值

状态转移

任选节点\(k\),以\(k\)点为根构成的加分二叉树的值=它的左子树的最大加分值 \(\times\) 右子树的最大加分值 \(+\) \(k\)点权值

\[\large f[i][j] = max(f[i][j], f[i][k - 1] * f[k + 1][j] + w[k]) (i <= k <= j) \]

\(Q\):石子合并枚举的\(k\)满足条件为\(i <= k < j\),为什么本题是\(i<=k<=j\)呢?

答:

  • 石子合并至少需要两堆才能合并,即有\(k\),也必须有\(k + 1\),才能划分成两个区间。
    换句话说,就是\(i<=k,k+1<=j\),也就是\(i<=k<j\)

  • 回到本题,因为左子树右子树可以为空,也就是如果\(i=k\)时,左子树为空,如果\(j=k\)时,右子树为空, 题目中明确:若某个子树为空,规定其加分为 \(1\),也就是此时\(ls=1,rs=1\)就可以得到正确的得分了。

前序序列

我们只需要获取一个\(f[i][j]\)的时候,同时开一个相同大小的数组\(g[i][j]\),存储此区间内的根节点是谁,其实这是一个递归定义,我们捋着这条线索,就可以一路向上(或向下),找出完整的二叉树形态,也就是在中序序列确定情况下的前序序列,捋的办法就是用递归。

\(Q\):如果存在多种方案,则输出字典序最小的方案。

: 因为\(k\)是从小到大遍历的,所以肯定是小的先存进去。而对比时采用了小于号,只有更小的才能更新,就保证取得的节点是字典序最小的方案。

另外一种就是不用记录数组,就是现推,现看\(f[1][n]\)是从哪个值转移过来的,边计算边退,两个都是一样的。

四、\(DP\)代码

#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;
}

posted @ 2022-01-07 14:13  糖豆爸爸  阅读(114)  评论(0编辑  收藏  举报
Live2D