加分二叉树

GitHub跳转原题关系图返回列表

形式化题目

给定 nn 个节点的分数 d1,d2,…,dnd_1, d_2, \dots, d_n(正整数)。在所有中序遍历恰好为 (1,2,…,n)(1,2,\dots,n) 的二叉树中,定义任一子树的加分为

subtree 的加分=左子树的加分×右子树的加分+根的分数\text{subtree 的加分} = \text{左子树的加分} \times \text{右子树的加分} + \text{根的分数}

其中空子树的加分规定为 11,叶子的加分就是叶节点本身的分数(不考虑它的空子树,即叶子不套用上面的乘加公式)。

求:最高加分,以及取到该加分的那棵树的前序遍历。

例如 n=5n=5、分数为 5, 7, 1, 2, 105,\ 7,\ 1,\ 2,\ 10 时,最高加分是 145145,对应的树如下图(每个节点标出它的加分算式),其前序遍历为 3 1 2 4 53\ 1\ 2\ 4\ 5:

graph TD
    n3["3:12×12+1=145"] -->|左| n1["1:1×7+5=12"]
    n3 -->|右| n4["4:1×10+2=12"]
    n1 -->|右| n2["2:叶子 7"]
    n4 -->|右| n5["5:叶子 10"]

观察这棵树:中序恰好是 1 2 3 4 51\ 2\ 3\ 4\ 5(每个节点的左子树编号都在它左边);每个节点的加分都按「左 ×\times 右 ++ 根」算出,而叶子直接取自身分数;从根 3 出发先根、再左、再右,读出的正是 3 1 2 4 53\ 1\ 2\ 4\ 5。

正解

思路

朴素想法与瓶颈。 最直接的做法是枚举所有中序为 (1,…,n)(1,\dots,n) 的二叉树形态,逐棵算分取最大。但形态数是 Catalan 数,n=30n=30 时约 101610^{16},枚举整棵树这条路走不通。要绕开它,需要回答:形态为什么多?因为我们在枚举「整棵树」;而题目其实允许把树拆开看——

观察一:加分公式左右独立。 乘法两侧的子树只以「加分标量」的身份相乘,左子树怎么长完全不影响右子树的最优性。所以以 kk 为根时,左右两段可以各自独立地取最高,再用乘法拼起来(加分全为正数,乘法单调,局部最优可拼成全局最优)。

观察二:中序固定 ⇒ 子树是连续区间。 中序遍历为 1..n1..n 时,以 kk 为根的子树,其节点必然是根左侧的一段 [l,k−1][l,k-1] 和右侧的一段 [k+1,r][k+1,r],不会交错。于是「所有形态」被压缩成「所有区间 ×\times 所有根」,共 O(n2)O(n^2) 个子问题——这正是区间 DP 的标准结构。

状态与转移。 设 f[l][r]f[l][r] 为中序是区间 [l,r][l,r] 的子树能取得的最高加分:

f[l][r]=max⁡l⩽k⩽r(f[l][k−1]×f[k+1][r]+dk)(l<r)f[l][r] = \max_{l \leqslant k \leqslant r} \Big( f[l][k-1] \times f[k+1][r] + d_k \Big) \qquad (l<r)

边界:空区间 f=1f=1;叶子是特例,f[i][i]=dif[i][i]=d_i(题面要求叶子不套乘加公式,若写成 1×1+di1\times1+d_i 全题都会算错)。按区间长度从小到大求解,短区间先算完,长区间才能引用它们。

样例的 DP 表。 下表是样例 5, 7, 1, 2, 105,\ 7,\ 1,\ 2,\ 10 的完整状态,单元格写成 加分/根(即 f[l][r] / root[l][r]f[l][r]\,/\,root[l][r]),行列下标是区间的左右端点:

l\rl \backslash r 1 2 3 4 5
1 5/1 12/1 13/1 25/3 145/3
2 7/2 8/2 15/3 85/3
3 1/3 3/3 13/3
4 2/4 12/4
5 10/5

观察三点:对角线是叶子,直接等于 did_i;计算 [1,5][1,5] 时用到的子段(如 [1,2][1,2]、[4,5][4,5])都在它的左下方,所以按长度递推是安全的;右上角 145/3 给出最终答案——根是 3,左段 [1,2][1,2] 最优根是 1、右段 [4,5][4,5] 最优根是 4。逐层沿最优根往下走,就还原出了上文那棵树。

前序遍历还原。 算分时同时记录每段的最优根 root[l][r]root[l][r](多个根同分时取最靠左的,保证答案确定、与评测数据一致),前序遍历就是「先根、再左段、再右段」:用栈迭代,弹出 [l,k][l,k] 输出 kk,先压右段再压左段,使左段先弹出。样例中即 3→[1,2]→1, [2,2]→2→[4,5]→4, [5,5]→53 \to [1,2] \to 1,\ [2,2] \to 2 \to [4,5] \to 4,\ [5,5] \to 5,得到 3 1 2 4 5。

最终答案在 f[1][n]f[1][n]。对应到代码,best(l, r) 的返回值 (最高加分, 最优根) 正是 f[l][r]f[l][r] 与 root[l][r]root[l][r] 的打包:它既参与上层转移,也在前序还原时直接查根,solve() 只负责读入、调用和输出。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-10 11:30
 * update_at: 2026-10-10 11:30
 */
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

int n;
ll d[35];
ll score[35][35];   // 区间最高加分
int root[35][35];   // 取到最高加分的根

// best(l, r):返回 (最高加分, 根)
ll best(int l, int r) {
    if (l > r) return 1;
    if (l == r) { root[l][r] = l; return d[l]; }
    if (score[l][r] != -1) return score[l][r];
    ll top = 0; int top_k = l;
    for (int k = l; k <= r; k++) {
        ll s = best(l, k - 1) * best(k + 1, r) + d[k];
        if (s > top) { top = s; top_k = k; }
    }
    score[l][r] = top;
    root[l][r] = top_k;
    return top;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> d[i];
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= n; j++)
            score[i][j] = -1;
    cout << best(1, n) << "\n";

    // 前序遍历:根 -> 左 -> 右
    vector<int> order;
    vector<pair<int, int> > stack;
    stack.push_back(make_pair(1, n));
    while (!stack.empty()) {
        int l = stack.back().first, r = stack.back().second;
        stack.pop_back();
        if (l <= r) {
            int k = root[l][r];
            order.push_back(k);
            stack.push_back(make_pair(k + 1, r));
            stack.push_back(make_pair(l, k - 1));
        }
    }
    for (size_t i = 0; i < order.size(); i++) {
        if (i) cout << " ";
        cout << order[i];
    }
    cout << "\n";
    return 0;
}
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 05:17
# update_at: 2026-10-02 05:17

import sys
from functools import cache


@cache
def best(l: int, r: int, d: tuple[int, ...]) -> tuple[int, int]:
    """中序为 l..r 的子树的 (最高加分, 取到最高的根);空区间加分为 1、根记 0。

    根 k 的加分 = 左子树加分 × 右子树加分 + d[k],两棵子树各自独立地也取最高,
    故按区间长度递推;叶子的加分就是它本身的分数(不套空子树规则),
    同分时取最靠左的根,保证前序遍历与标准答案一致。
    """
    if l > r:
        return (1, 0)
    if l == r:
        return (d[l], l)
    top, top_k = 0, l
    for k in range(l, r + 1):
        left = best(l, k - 1, d)[0]
        right = best(k + 1, r, d)[0]
        score = left * right + d[k]
        if score > top:
            top, top_k = score, k
    return (top, top_k)


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    d: tuple[int, ...] = (0,) + tuple(next(data) for _ in range(n))  # 下标 1..n 的分数

    top_score, _ = best(1, n, d)

    # 前序遍历:根 → 左子树 → 右子树;用栈把"先压右再压左"倒成正确的出栈顺序
    order: list[int] = []
    stack = [(1, n)]
    while stack:
        l, r = stack.pop()
        if l <= r:
            k = best(l, r, d)[1]
            order.append(k)
            stack.append((k + 1, r))
            stack.append((l, k - 1))

    print(top_score)
    print(*order)


if __name__ == "__main__":
    solve()

复杂度

  • 时间复杂度:O(n3)O(n^3)。区间数 O(n2)O(n^2),每段枚举根 O(n)O(n);记忆化保证每个区间只算一次。
  • 空间复杂度:O(n2)O(n^2)。缓存所有区间的 (加分, 根),外加 O(n)O(n) 的前序栈。n<30n<30 时实测单点约 0.015 s、峰值内存约 15 MB,相对 1000 ms / 32 MB 的限制非常宽裕。

总结

这道题的关键是两次视角转换:把「枚举树形态」换成「枚举区间与根」——因为中序遍历固定为 1..n1..n,任何子树都是连续编号的一段,根把它劈成左右两个独立子问题;再利用加分公式里左右子树互不干扰,两侧各自取最大即可拼出全局最优。

两个容易踩的点:一是叶子边界,题面规定叶子加分就是 did_i,不能套 1×1+d1\times1+d 的公式(套了样例都会算错);二是前序还原,分数之外必须记录每段的最优根,并约定同分取最左,否则同一最高加分可能对应多棵不同的树,前序输出不确定。最终用一个 O(n3)O(n^3) 的区间 DP 同时拿到分数与结构,7 组真实测试数据全部通过。