形式化题目
给定 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),代价恰好是"被消去的标记 × 它当时的两侧邻居"。
前两步的两侧邻居是 (a4,a2) 与 (a4,a3);第 3 步消去 a3 时环上只剩
a3,a4 两个标记,a3 的两侧都是同一个 a4=10,所以代价是 10⋅5⋅10=500,
最终珠子正是 (a4,a4)=(10,10),与上面的说法一致。
正解
思路
朴素做法与它的瓶颈。 最直接的想法是枚举所有聚合顺序:每一步从当前剩下的相邻珠对中
任选一对合并,递归到只剩一颗。N=100 时第一层就有 100 种选择、第二层 99 种……
规模是 N! 量级。但真正的瓶颈不是枚举量,而是状态描述选错了:它把"当前项链长什么样"
当作状态,而项链形状有指数多种,其中大量形状其实对应同一个子问题。
换个角度观察样例。((4⊕1)⊕2)⊕3 的每一步都只处理一段连续的珠子:
先把第 4、1 颗合成 (10,3),再依次往右并。这段珠子的内部怎么聚合,和它外面的珠子毫无关系。
所以状态应该是"哪一段连续珠子",而不是"整条项链"。
关键观察:盯住最优方案的"最后一次聚合"。 设某段连续珠子 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,k)+F(k+1,j)+aiak+1aj+1。
两边夹逼,F(i,j) 就是最优值。
把珠串看成一链时这个视角更好用:链上的段 ai,ai+1,…,aj 聚成一颗珠子,
要消去的是内部标记 ai+1,…,aj,两端 ai 与 aj+1 保留下来,所以聚成的那颗
珠子是 (ai,aj+1),不是 (ai,aj)。这正是区间 DP 里"第 j+1 个标记"
这个下标的来源:区间长度每增加 1,就多消去一个标记、尾标记就往后挪一位。
(尾标记会是段尾的下一个,是因为第 j 颗珠子本来就是 (aj,aj+1)。)
状态与转移。 记
F(i,j)=把连续珠子 ai,…,aj 全部聚成一颗时释放的最大能量,其中段 ai,…,aj 一共是 j−i+1 颗珠子、j−i 次聚合,消去的是内部标记
ai+1,…,aj,聚成的那颗珠子保留两端标记,即 (ai,aj+1)——
注意是 aj+1 而不是 aj(若写成 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(i,i+L−1) 的值由该行上一行(更短的区间)拼出。例如 F(4,5)=a4a5a6=10⋅2⋅3=60,
L=3 的五个值都是 210。
再单独展开目标格 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 次转移(状态 14751 个)。代码用显式双层循环按长度递推,而不是 @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
49
50
51
52
53
54
55/**
* 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 13:00
* update_at: 2026-10-10 13:00
*/
// 能量项链:环拆链 + 区间 DP
#include <cstdio>
using namespace std;
typedef long long ll;
const int MAXS = 1024; // 2N 的上界
ll a[MAXS]; // 环复制一倍后的标记序列
// f[i][j]:把 a[i..j] 这串连续珠子聚成一颗时释放的最大能量
ll f[MAXS][MAXS];
int main() {
int n;
if (scanf("%d", &n) != 1) {
return 0;
}
for (int i = 0; i < n; i++) {
scanf("%lld", &a[i]);
a[i + n] = a[i];
}
int size = 2 * n;
for (int length = 2; length <= n; length++) { // 长度超过 n 的区间不是环上的连续段
for (int i = 0; i + length - 1 < size - 1; i++) { // 右端留一个 a[j+1] 当尾标记
int j = i + length - 1;
ll head = a[i], tail = a[j + 1];
ll best = 0;
for (int k = i; k < j; k++) { // 最后一步:[i,k] 与 [k+1,j] 各自的珠子再聚合
ll energy = f[i][k] + f[k + 1][j] + head * a[k + 1] * tail;
if (energy > best) {
best = energy;
}
}
f[i][j] = best;
}
}
ll ans = 0; // 环上每种起点都对应一个长度为 n 的区间
for (int i = 0; i < n; i++) {
if (f[i][i + n - 1] > ans) {
ans = f[i][i + n - 1];
}
}
printf("%lld\n", ans);
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-01 22:56
# update_at: 2026-10-01 22:56
import sys
def best_energy(marks: list[int], n: int) -> int:
"""项链上 n 颗珠子全部聚合时能释放的最大总能量。"""
a = marks * 2 # 环拆成链再复制一倍:任何区间 [i, i+n-1] 都是环上的一种起点
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)] # 只顺序取 N 个标记:多余 token 不再消费
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 个标记,
而不是把整行都当成标记:仓库里个别评测数据的标记个数多于 N,按 N 截断更稳。
复杂度
- 时间复杂度:状态 O(N2),每个状态枚举 k 转移 O(N),合计 O(N3)。
朴素的顺序枚举是 O(N!) 量级。
- 空间复杂度:f 是 2N×2N 的整数矩阵,O(N2)。
- 实测 N=100 随机数据 0.053 s / 16.4 MB(OJ 限时 1000 ms、内存 128 MB)。
总结
本题做了两次关键转换:
- 把"枚举聚合顺序"换成"枚举最后一步的切点"。前者是 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) 候选表。