题目描述

牛客原题: ✅ 补充题 132. 加分二叉树

给你一个正整数数组 scores,表示编号为 1 到 n 的节点分数,节点 i 的分数为 scores[i - 1]。请用全部节点构造一棵二叉树,使中序遍历的节点编号依次为 1, 2, …, n。

一棵子树的得分按以下规则计算:

  • 空子树的得分为 1。
  • 叶子节点的得分就是它自身的分数。
  • 非叶子子树的得分为 左子树得分 × 右子树得分 + 根节点分数。

请返回能够获得的最大得分,以及任意一棵最优二叉树的前序编号序列。结果对象分别保存任意精度整数得分和前序数组。

示例 1:

输入: scores = [5,7,1,2,10]
输出: 最大分数 = 145, 前序编号 = [3,1,2,4,5]
解释: 选 3 为根,左子树得分为 12,右子树得分为 12,所以总分为 12×12+1=145;中序编号仍为 1、2、3、4、5。

提示:

  • 节点编号为 1…n,必须构成中序遍历顺序。
  • 节点分数为正整数。
  • 空子树得分为 1,叶子得分直接等于自身分数。
  • 返回大整数得分和一棵最优树的前序编号。

题意分析

中序编号固定后,根的编号就确定了左右子树各自使用的连续区间,不能随意分配节点。因此枚举根后,左右两部分都变成同类型的更小区间问题,可以用区间动态规划。

解法:区间枚举根并记录最优结构

核心思路

[!blue]

dp[l][r] 表示编号对应区间内能够获得的最大得分,roots[l][r] 记录取得该分数的根下标。选择 k 为根时,左区间为 [l,k-1],右区间为 [k+1,r];空区间按得分 1 处理。

节点分数都是正数,乘积随左右得分增大而增大,所以固定根后,两边可以各自取最大值,再计算 left * right + scores[k]。枚举所有根并取最大,就覆盖了当前区间全部合法树形。单节点必须单独初始化为自身分数,不能错误套成 1 * 1 + score。

按区间长度递增计算,使依赖的子区间先完成。最后从完整区间出发,依次输出根编号、左区间前序、右区间前序。乘加使用大整数,Go 用新的 big.Int 保存候选,避免改写已存的子问题结果。

解题步骤

  1. 单节点区间直接取节点分数,空子树按乘法单位元 1 处理。
  2. 按区间长度递增枚举根,以左分数乘右分数再加根分数更新最优值。
  3. 保存取得最优值的根下标,同分时保留先枚举的较小根。
  4. 根据根表按根、左、右恢复前序编号,并返回大整数分数。

代码实现

class Solution {
    static class Result {
        BigInteger score;
        int[] preorder;

        Result(BigInteger score, int[] preorder) {
            this.score = score;
            this.preorder = preorder;
        }
    }

    private void preorder(int l, int r, int[][] roots, List<Integer> out) {
        if (l > r) {
            return;
        }

        int k = roots[l][r];

        out.add(k + 1);
        preorder(l, k - 1, roots, out);
        preorder(k + 1, r, roots, out);
    }

    public Result bestTree(int[] scores) {
        int n = scores.length;
        BigInteger[][] dp = new BigInteger[n][n];
        int[][] roots = new int[n][n];

        for (int i = 0; i < n; i++) {
            dp[i][i] = BigInteger.valueOf(scores[i]);
            roots[i][i] = i;
        }

        for (int len = 2; len <= n; len++) {
            for (int l = 0; l + len <= n; l++) {
                int r = l + len - 1;

                for (int k = l; k <= r; k++) {
                    BigInteger left = k == l ? BigInteger.ONE : dp[l][k - 1];
                    BigInteger right = k == r ? BigInteger.ONE : dp[k + 1][r];
                    BigInteger value = left.multiply(right).add(BigInteger.valueOf(scores[k]));

                    if (dp[l][r] == null || value.compareTo(dp[l][r]) > 0) {
                        dp[l][r] = value;
                        roots[l][r] = k;
                    }
                }
            }
        }

        List<Integer> list = new ArrayList<>();

        preorder(0, n - 1, roots, list);
        int[] order = list.stream().mapToInt(Integer::intValue).toArray();

        return new Result(dp[0][n - 1], order);
    }
}
import "math/big"

type TreeResult struct {
    Score    *big.Int
    Preorder []int
}

func bestTree(scores []int) TreeResult {
    n := len(scores)
    dp := make([][]*big.Int, n)
    roots := make([][]int, n)
    for i := range dp {
        dp[i] = make([]*big.Int, n)
        roots[i] = make([]int, n)
        dp[i][i] = big.NewInt(int64(scores[i]))
        roots[i][i] = i
    }
    for length := 2; length <= n; length++ {
        for l := 0; l+length <= n; l++ {
            r := l + length - 1
            for k := l; k <= r; k++ {
                left, right := big.NewInt(1), big.NewInt(1)
                if k > l {
                    left = dp[l][k-1]
                }
                if k < r {
                    right = dp[k+1][r]
                }
                value := new(big.Int).Mul(left, right)
                value.Add(value, big.NewInt(int64(scores[k])))
                if dp[l][r] == nil || value.Cmp(dp[l][r]) > 0 {
                    dp[l][r] = value
                    roots[l][r] = k
                }
            }
        }
    }
    order := []int{}
    var visit func(int, int)
    visit = func(l, r int) {
        if l > r {
            return
        }
        k := roots[l][r]
        order = append(order, k+1)
        visit(l, k-1)
        visit(k+1, r)
    }
    visit(0, n-1)
    return TreeResult{dp[0][n-1], order}
}

复杂度分析

  • 时间复杂度:$O(n^3)$ 次大整数乘加与比较,另需 $O(n)$ 次节点访问恢复前序;实际位运算成本还取决于得分长度。
  • 空间复杂度:$O(n^2)$ 个大整数及根下标,另有最多 $O(n)$ 的重建调用栈和输出。

关键点总结

[!green]

固定中序把结构选择化成区间根选择;正分数保证左右子树可以各自取最优,再通过根表恢复具体结构。

易错点总结

[!yellow]

叶子不是1×1+d;空子树取1;重建时输出节点编号,不是分数;不要在比较之前取模或转int。

相似题目

题目 难度 关联与区别
96. 不同的二叉搜索树 中等 同样按根拆成左右中序区间,原题统计树的数量,本题优化分数。
95. 不同的二叉搜索树 II 中等 都依赖中序区间决定左右子树,本题只记录最优根并恢复一棵树,不枚举全部结构。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/63947391
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!