形式化题目
给定 N 个正整数 a1,a2,…,aN,把它们看作一个环(即 aN+1=a1)。
第 i 颗珠子是二元组 (ai,ai+1)。
一次聚合取相邻的两颗珠子 (m,r) 与 (r,n),把它们替换成一颗珠子 (m,n),
并释放 m⋅r⋅n 单位的能量。做 N−1 次聚合后只剩一颗珠子,
求总能量的最大值。
也可以换个说法:一次聚合就是消去当前环上的一个标记,代价是它此时两侧邻居的标记
与它自己的乘积;每消去一个标记,环上的珠子就少一颗。做 N−1 次聚合就是把 N 个标记
中的 N−1 个消去,最后剩下一个标记 as,而那颗最终珠子是 (as,as)——
头尾都是它自己,因为环已经没有"断口"了。
以样例 N=4、a=(2,3,5,10) 为例,四颗珠子是 (2,3),(3,5),(5,10),(10,2)。
题面给出的最优顺序是 ((4⊕1)⊕2)⊕3,逐步展开如下:
| 步骤 |
聚合操作 |
释放能量 |
累计能量 |
| 1 |
(10,2)⊕(2,3)→(10,3) |
10⋅2⋅3=60 |
60 |
| 2 |
(10,3)⊕(3,5)→(10,5) |
10⋅3⋅5=150 |
210 |
| 3 |
(10,5)⊕(5,10)→(10,10) |
10⋅5⋅10=500 |
710 |
观察重点:每次聚合确实都消去了一个标记(依次是 a1=2、a2=3、a3=5,
最后只剩 a4=10),代价恰好是"被消去的标记 × 它当时的两侧邻居"。
第 3 步消去 a3 时环上只剩 a3,a4 两个标记,a3 的两侧都是同一个
a4=10,所以代价是 10⋅5⋅10=500,最终珠子正是 (a4,a4)=(10,10)。
正解
思路
朴素做法与它的瓶颈。 最直接的想法是枚举所有聚合顺序:每一步从当前剩下的相邻珠对中
任选一对合并,递归到只剩一颗。N=100 时第一层就有 100 种选择、第二层 99 种……
规模是 N! 量级。但真正的瓶颈不是枚举量,而是状态描述选错了:它把"当前项链
长什么样"当作状态,而项链形状有指数多种,其中大量形状其实对应同一个子问题——
一段连续珠子内部怎么聚合,和它外面的珠子毫无关系(聚合只发生在相邻珠子之间,
段内操作不会改变段两端露出的标记)。
关键观察:盯住最优方案的"最后一次聚合"。 设某段连续珠子 ai,ai+1,…,aj
最终被聚成一颗。它的最后一次聚合必然把这段珠子分成左右两部分
[i,k]与[k+1,j](i⩽k<j),因为它们在这最后一步之前必须各自已经是一颗完整的珠子。设左边聚成了 (ai,ak+1)
(头标记是段首 ai,尾标记是分界点的下一个标记 ak+1),右边聚成了
(ak+1,aj+1),那么这一步释放
ai⋅ak+1⋅aj+1.两部分内部的聚合互相不影响,总能量直接相加。于是只要左右两段各自取得内部最优,
整体就最优——这是标准的"最优子结构",可以用区间 DP。
正确性两边都说一遍:任取 [i,j] 的一个合法顺序,它的最后一步一定对应某个切点 k,
而这之前的所有聚合都发生在 [i,k] 与 [k+1,j] 内部,所以它的总能量
⩽f(i,k)+f(k+1,j)+aiak+1aj+1⩽f(i,j);
反过来,任取一个 k,把实现 f(i,k) 与 f(k+1,j) 的两个顺序依次做一遍,再做
最后一次聚合,就得到一个真实合法的顺序,能量恰为三项之和。两边夹逼,f(i,j)
就是最优值。
状态与转移。 记
f(i,j)=把连续珠子 ai,…,aj 全部聚成一颗时释放的最大能量,其中段 ai,…,aj 一共是 j−i+1 颗珠子、j−i 次聚合,消去的是内部标记
ai+1,…,aj,聚成的那颗珠子保留两端标记,即 (ai,aj+1)——
注意尾标记是 aj+1 而不是 aj(第 j 颗珠子本来就是 (aj,aj+1);
若误写成 aj,样例恰好仍输出 710,但 N=4、a=(49,6,43,6) 这类数据立刻出错)。
初始 f(i,i)=0,转移为
f(i,j)=i⩽k<jmax{f(i,k)+f(k+1,j)+ai⋅ak+1⋅aj+1}.转移只依赖长度更小的区间,所以按区间长度 L=2,3,…,N 从小到大递推即可。
环拆链并复制一倍。 上面的 DP 处理的是"链"。环上没有天然的起点,但把环从任意位置
剪开并复制一倍,得到长度为 2N 的序列。环上任何一种聚合方案都恰好对应一个长度为 N
的窗口 [i,i+N−1](1⩽i⩽N);不同起点可能给出不同的最优值,所以答案是
1⩽i⩽Nmaxf(i,i+N−1).用样例验证。 复制一倍得 a=(2,3,5,10,2,3,5,10),小规模 F 值如下
(行是区间长度 L,列是区间起点 i;单元格是 F(i,i+L−1);— 表示该窗口
超出复制后的序列):
| L\i |
1 |
2 |
3 |
4 |
5 |
| 1 |
F(1,1)=0 |
0 |
0 |
0 |
0 |
| 2 |
F(1,2)=30 |
150 |
100 |
60 |
30 |
| 3 |
F(1,3)=210 |
210 |
210 |
210 |
210 |
| 4 |
F(1,4)=250 |
300 |
460 |
710 |
— |
每个格子由上一行(更短的区间)拼出,例如 F(4,5)=a4a5a6=10⋅2⋅3=60。
再单独展开目标格 F(4,7)(区间 (10,2,3,5),尾标记 a8=10),它有三个候选切点:
| k |
f(4,k) |
f(k+1,7) |
a4⋅ak+1⋅a8 |
合计 |
| 4 |
0 |
f(5,7)=210 |
10⋅2⋅10=200 |
410 |
| 5 |
f(4,5)=60 |
f(6,7)=150 |
10⋅3⋅10=300 |
510 |
| 6 |
f(4,6)=210 |
0 |
10⋅5⋅10=500 |
710 |
取 k=6,含义是"先聚出左珠 (10,5)、右珠 (5,10),最后合并",正是题面的
((4⊕1)⊕2)⊕3。最后,四个窗口的答案是 250,300,460,710,
取最大得 710——这也说明必须枚举起点:只算 f(1,N)=250 会得到错误答案。
实现选择。 状态数 O(N2)、每个状态转移 O(N),总复杂度 O(N3),N⩽100
时转移约 6.6×105 次。代码用显式双层循环按长度递推,而不是 @cache 记忆化搜索:
迭代版没有函数调用和哈希开销,N=100 时实测约 0.05 s,相对 1000 ms 的限时余量充足。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/**
* 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 a[205]; // marks 复制一倍
ll f[205][205];
ll best_energy() {
int size = 2 * n;
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
f[i][j] = 0;
for (int length = 2; length <= n; length++) {
for (int i = 0; i < size - length; i++) {
int j = i + length - 1;
ll head = a[i], tail = a[j + 1];
for (int k = i; k < j; k++) {
ll energy = f[i][k] + f[k + 1][j] + head * a[k + 1] * tail;
if (energy > f[i][j]) f[i][j] = energy;
}
}
}
ll ans = 0;
for (int i = 0; i < n; i++)
if (f[i][i + n - 1] > ans) ans = f[i][i + n - 1];
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
a[i + n] = a[i];
}
cout << best_energy() << "\n";
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40#!/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 06:54
# update_at: 2026-10-02 06:54
import sys
def best_energy(marks: list[int], n: int) -> int:
"""项链上 n 颗珠子全部聚合时能释放的最大总能量。"""
a = marks * 2 # 环拆成链再复制一倍:任何长度为 n 的窗口都是环上的一种起点
size = 2 * n
# f[i][j]:把 a[i..j] 这串连续珠子聚成一颗时释放的最大能量;长度为 1 时无需聚合,值为 0
f = [[0] * size for _ in range(size)]
for length in range(2, n + 1): # 长度超过 n 的区间不是环上的连续段,不必计算
for i in range(size - length): # 右端留一个 a[j+1] 当聚合后珠子的尾标记
j = i + length - 1
head, tail = a[i], a[j + 1] # [i,j] 全部聚合后得到珠子 (head, tail)
row = f[i]
for k in range(i, j): # 最后一步:[i,k] 与 [k+1,j] 各自的珠子再聚合
energy = row[k] + f[k + 1][j] + head * a[k + 1] * tail
if energy > row[j]:
row[j] = energy
# 环上每种起点都对应一个长度为 n 的区间,取最大者
return max(f[i][i + n - 1] for i in range(n))
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
marks = [next(data) for _ in range(n)]
print(best_energy(marks, n))
if __name__ == "__main__":
solve()
代码中 a = marks * 2 完成"环拆链并复制一倍";f[i][j] 就是 f(i+1,j+1)
(0 起下标);for length in range(2, n + 1) 保证递推顺序,head * a[k + 1] * tail
对应 ai⋅ak+1⋅aj+1。读入处用 data[1 : n + 1] 严格只取
N 个标记,而不是把整行都当成标记:这样即使评测数据尾部多出 token 也按 N 截断,更稳。
复杂度
- 时间复杂度:状态 O(N2),每个状态枚举 k 转移 O(N),合计 O(N3)。
朴素的顺序枚举是 N! 量级。
- 空间复杂度:f 是 2N×2N 的整数矩阵,O(N2)。
- 实测 N=100 随机数据 0.052 s、峰值约 15 MB(OJ 限时 1000 ms)。
总结
本题做了两次关键转换:
- 把"枚举聚合顺序"换成"枚举最后一步的切点"。前者是 N! 量级的搜索,后者只需要
记录"哪一段连续珠子",因为最后一次聚合必然把这段珠子劈成两半、两半互不影响。
写转移时务必记住长度为 L 的区间 [i,j] 聚成的是 (ai,aj+1),尾标记是段尾的
下一个标记。
- 把环拆成链并复制一倍。DP 只处理链,而环上共有 N 种起点,复制后每种起点对应一个
长度 N 的窗口,答案取这些窗口的最大值;只算 f(1,N) 在样例上就会少算。
图示解析
这张图串起本题从建模到得到答案的主线:
1
2
3
4
5
6
7
8输入:环上 N 颗珠子,第 i 颗为 (a_i, a_{i+1}),下标模 N
`- 建模:聚合 = 消去环上的一个中间标记 a_r,累加 左*中*右
`- 关键观察:最优方案的"最后一次聚合"把连续段 [i, j] 劈成 [i, k] 与 [k+1, j]
|- 两段内部聚合互不影响,可分别取最优(最优子结构)
|- 本步释放 a_i * a_(k+1) * a_(j+1) ← 尾标记是段尾的"下一个"
`- 状态 f(i, j):把 a_i..a_j 聚成一颗的最大能量,按区间长度从小到大递推
`- 环拆链复制一倍:环上 N 种起点 = 长度 N 的窗口
`- 答案 = max_i f(i, i+N-1) —— 样例中窗口 250 / 300 / 460 / 710 取 710
先看"消去中间标记"这一层建模:它把题面的物理动作翻译成了一个纯组合问题。
再沿着"最后一次聚合"这一条分支往下走,每一步都只用到上一步的结论:
分离性给出状态定义,状态定义给出递推式,递推式加上"环拆链复制一倍"给出答案位置。
图中只保留这三层关系,样例的具体数值推演见前文的聚合过程表与 f(4,7) 候选表。