You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 479. 加分二叉树

一、题目描述

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

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

subtree的左子树的加分 × subtree的右子树的加分 subtree的根的分数 

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

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

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

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

输入格式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]:从ij区间内,选一点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;
}