题单 · 习题解析

洛谷 P1063 [NOIP 2006 提高组] 能量项链

★★ 区间的端点是「标记」不是「珠子」(写成 f[k+1][r] 顶格 300/300 被抓,而样例放过);★★★ 而「E ≤ 2.1×10⁹」不是推出来的,是出题人替你挡掉的 —— 顺手随机 60/300 违反它

原题:洛谷 P1063出自 第 26 章 区间 DP:石子合并 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1063,日期见页头。两边不一致时信原站。

题目描述

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N 颗能量珠。 能量珠是一颗有头标记尾标记的珠子,这些标记对应着某个正整数。 并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。 因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用, 这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。 如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n, 则聚合后释放的能量为 m × r × n(Mars 单位),新产生的珠子的头标记为 m,尾标记为 n

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。 显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N = 4,4 颗珠子的头标记与尾标记依次为 (2,3) (3,5) (5,10) (10,2)。 我们用记号 表示两颗珠子的聚合操作,(j ⊕ k) 表示第 j, k 两颗珠子聚合后所释放的能量。 则第 4、1 两颗珠子聚合后释放的能量为:

(4 ⊕ 1) = 10 × 2 × 3 = 60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

(((4 ⊕ 1) ⊕ 2) ⊕ 3) = 10 × 2 × 3 + 10 × 3 × 5 + 10 × 5 × 10 = 710

输入格式

第一行是一个正整数 N4 ≤ N ≤ 100),表示项链上珠子的个数。 第二行是 N 个用空格隔开的正整数,所有的数均不超过 1000。 第 i 个数为第 i 颗珠子的头标记(1 ≤ i ≤ N), 当 i < N 时,第 i 颗珠子的尾标记应该等于第 i+1 颗珠子的头标记。 第 N 颗珠子的尾标记应该等于第 1 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉, 随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式

一个正整数 EE ≤ 2.1 × 10⁹),为一个最优聚合顺序所释放的总能量。

说明/提示

NOIP 2006 提高组 第一题

输入输出样例

输入

4
2 3 5 10

输出

710

四颗珠子 (2,3) (3,5) (5,10) (10,2) 成环。题面里那个顺序释放 10×2×3 + 10×3×5 + 10×5×10 = 710

⚠ 这一组样例挡住了「没破环」(250), ★ 而对本页那个最典型的坑 —— 断点写成 f[k+1][r] —— 完全无能为力(照样打 710)。

1★★ 关键的一步(一):区间的端点是「标记」,不是「珠子」

这道题看着像石子合并换了个代价公式,但它多了一层转译。

★ 一颗珠子 = 两个相邻标记夹出来的那一段
    标记:  a₁      a₂      a₃      a₄     (a₁)
    珠子:      ①       ②       ③       ④

把相邻两颗珠子聚合,吃掉的正是它们中间那个标记(a_l, a_k)(a_k, a_r) 合成 (a_l, a_r),释放 a_l × a_k × a_r

⇒ 于是转移写成:

    f[l][r] = max over k in (l, r) of ( f[l][k] + f[k][r] + a[l] × a[k] × a[r] )

⚠⚠ 注意是 f[l][k] + f[k][r],不是 f[k+1][r] 石子合并里写 k+1,是因为那里的区间装的是,一堆要么归左要么归右; 而这里 k 是一个标记,它同时是左半的右端和右半的左端

p1063.cpp★ 这一版就能 AC
// P1063 [NOIP 2006 提高组] 能量项链 —— ★ 这一版就能 AC。
//
// ★★ 关键的一步(一):**区间的端点是「标记」,不是「珠子」。**
// 第 i 颗珠子是 (aᵢ, aᵢ₊₁) —— 它由**两个相邻的标记**夹出来。
// 所以一段连续的珠子 = 一段连续的标记,而「合并」正好把中间那个标记吃掉:
//
// f[l][r] = max over k in (l, r) of ( f[l][k] + f[k][r] + a[l] × a[k] × a[r] )
//
// ⚠ 注意断点那两项是 `f[l][k]` 和 `f[k][r]` —— **k 同时是左半的右端和右半的左端**,
// 不是 `f[k+1][r]`。写成 `k+1` 就等于把「标记」当成「珠子」在切(见 p1063K1.cpp)。
//
// ★★ 关键的一步(二):**破环成链** —— 和 [P1880](/sol/p1880/) 一样,
// 把标记复制一遍(2N 个),跑长度为 N 的区间,答案取每个起点的最大值。
//
// ⚠ 题面那句「`E ≤ 2.1 × 10⁹`」正好卡在 `int` 上限 2 147 483 647 的**里面一点点**
// (余量只有 2.3%)—— 它是**出题人替你挡掉的**,不是能从数据范围推出来的
// (推出来的上界是 `99 × 1000³ ≈ 9.9 × 10¹⁰`)。这里照旧用 `long long`,
// 那样就不依赖那份信任。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (scanf("%d", &n) != 1 || n <= 0) return 0;
int m = 2 * n;
vector<long long> a(m + 2, 0);
for (int i = 1; i <= n; i++) {
if (scanf("%lld", &a[i]) != 1) return 0;
a[i + n] = a[i]; // 破环成链
}
a[m + 1] = a[1]; // 链上最后一个标记
// f[l][r]:把标记 l..r 之间那些珠子全合成一颗,最多能释放多少能量
vector<vector<long long>> f(m + 2, vector<long long>(m + 2, 0));
for (int len = 2; len <= n; len++) // len = 这一段有几颗珠子
for (int l = 1; l + len <= m + 1; l++) {
int r = l + len; // 标记下标:l..r 共 len 颗珠子
long long best = 0;
for (int k = l + 1; k < r; k++) // ★ k 是被吃掉的那个标记
best = max(best, f[l][k] + f[k][r] + a[l] * a[k] * a[r]);
f[l][r] = best;
}
long long ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, f[i][i + n]);
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★ 关键的一步(二):破环成链(和上一页一模一样)

项链是个环 ⇒ P1880 那一套原样搬过来:标记复制一遍, 跑「恰好 N 颗珠子」的区间,答案取每个起点的最大值。

p1063Line.cpp✗ 没破环(当成一条直的项链)
// ✗ P1063 的第一个坑:**没破环**(当成一条直的项链做)。
//
// 题面写得很清楚「项链」是一个环:第 N 颗珠子的尾标记等于第 1 颗的头标记。
// 这一版只跑 f[1][n+1],等于规定「必须最后合并第 N 颗和第 1 颗之间那一处」。
//
// ★ 方向能先说死:它只是把可行方案砍掉了一部分 ⇒ **答案恒 ≤ 正解**。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (scanf("%d", &n) != 1 || n <= 0) return 0;
vector<long long> a(n + 2, 0);
for (int i = 1; i <= n; i++) if (scanf("%lld", &a[i]) != 1) return 0;
a[n + 1] = a[1];
vector<vector<long long>> f(n + 2, vector<long long>(n + 2, 0));
for (int len = 2; len <= n; len++)
for (int l = 1; l + len <= n + 1; l++) {
int r = l + len;
long long best = 0;
for (int k = l + 1; k < r; k++)
best = max(best, f[l][k] + f[k][r] + a[l] * a[k] * a[r]);
f[l][r] = best;
}
printf("%lld\n", f[1][n + 1]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
≤ 正解 300 / 300(可行方案被砍掉了一部分)
300 轮被抓 251
官方样例 挡住了(250 vs 710)

3★★ 那个 `k+1`:几乎必被抓的 bug,官方样例偏偏放过了

p1063K1.cpp✗ 断点写成 f[k+1][r](把标记当珠子切)
// ✗ P1063 最典型的那个坑:断点写成了 `f[k+1][r]`。
//
// 石子合并里写的是 `f[l][k] + f[k+1][r]` —— 因为那里的区间装的是**堆**,
// 一个堆要么属于左边要么属于右边,不能两边都算。
//
// ⚠ 而这道题的区间端点是**标记**:`k` 这个标记**同时是左半的右端和右半的左端**
// (它正是那次合并被吃掉的那个标记)。所以必须写 `f[l][k] + f[k][r]`。
// 照着上一题的手感写成 `k+1`,就等于把标记当成了珠子在切。
//
// ★ 页面上量过:它**恒 ≤ 正解**(300/300),而且 `n` 一大就几乎必被抓
// (n ≤ 20 时 295/300,n ≤ 100 时 **300/300**)。
// ⚠⚠ 可**官方样例偏偏放过了它**(照样打出 710)—— 因为样例的 `n = 4`
// 正好是题面允许的最小值,切错一格几乎没差别。
// ⇒ 又一次「样例挡不挡得住,只能一个一个试」。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (scanf("%d", &n) != 1 || n <= 0) return 0;
int m = 2 * n;
vector<long long> a(m + 2, 0);
for (int i = 1; i <= n; i++) { if (scanf("%lld", &a[i]) != 1) return 0; a[i + n] = a[i]; }
a[m + 1] = a[1];
vector<vector<long long>> f(m + 2, vector<long long>(m + 2, 0));
for (int len = 2; len <= n; len++)
for (int l = 1; l + len <= m + 1; l++) {
int r = l + len;
long long best = 0;
for (int k = l + 1; k < r; k++)
best = max(best, f[l][k] + f[k + 1][r] + a[l] * a[k] * a[r]); // ✗ k+1
f[l][r] = best;
}
long long ans = 0;
for (int i = 1; i <= n; i++) ans = max(ans, f[i][i + n]);
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 只拧珠子个数(各 300 轮,标记值照题面 1~1000)
n 的上限 5 8 20 100(题面顶格)
≤ 正解 300 / 300 300 / 300 300 / 300 300 / 300
被抓 261 286 295 300 / 300

⇒ 这是个几乎必被抓的 bug ——⚠ 而官方样例放过了它。 原因很具体:样例的 n = 4,正好是题面允许的最小珠子数,切错一格几乎没有差别。

⇒ ★★ 本页因此给「样例是一测就死的过滤器」那条规律又添一个反例: 被样例放过的不一定是「偶尔才错」的 —— 也可能只是样例的规模太小

4★★★ 题面那句「E ≤ 2.1 × 10⁹」——它不是推出来的,是出题人替你挡掉的

输出格式那一行写着:一个正整数 EE ≤ 2.1 × 10⁹)。 而 int 的上限是 2 147 483 647 —— 2.1 × 10⁹ 正好在里面,余量只有 2.3%

⚠ 但这句保证和前面几道题的那些「保证」不是同一种东西

本书量过好几句「答案不超过 X」,它们都能从数据范围里乘出来P1164 那句正好是 32 位够用的充分条件P1064 那句真实上界 160 000 且可达P1060 那句松了 666 倍

这一句不行。 从数据范围能推出来的上界是:

    一次聚合最多 1000 × 1000 × 1000 = 10⁹,一共聚合 N − 1 = 99 次
    ⇒ E ≤ 99 × 10⁹ = 9.9 × 10¹⁰        —— 比 int 大 46 倍

⇒ 所以那句 E ≤ 2.1 × 10⁹出题人对测试数据的额外承诺 —— 它挡掉的是一整类合法但答案巨大的输入。 ★★★ 「保证」有两种:能从别处推出来的,和出题人替你挡掉的。前者可以信,后者要知道自己在信什么。

★★ 量一遍:顺手随机根本守不住那句保证
顶格 n = 100、标记全 1000 时的 E 99 000 000 000(正好撞上推出来的上界)
顺手随机(n ≤ 8、标记 1~1000)300 轮里 E > 2.1 × 10⁹ 60 轮(20%)
其中 E > int 上限 54
标记上限要压到多少才守得住 300

⇒ ★★ 题面那句保证,把「数据范围顶格」那种数据整个排除掉了 —— n = 100 且标记全 1000 是题目不会给的输入。 这是第 19 章 P1090 那条的又一次现场: 自己造顶格数据时,要连「输出侧的保证」一起满足,否则测的是一组题目不会给的输入。

p1063Int.cpp(全程 int 的那一版)照题面它是对的
★ 于是「用 int 行不行」有了一个精确的答案
生成器 int 版和正解不一致
标记 ≤ 300(守得住 E ≤ 2.1 × 10⁹ 0 / 300
标记 ≤ 1000(顺手随机,守不住) 54 / 300

★ 而那个 54 一个不差地等于「E 越过 int 上限」的 54 轮 —— 又一次「触发条件 ≡ 抓获数」。

⇒ 结论要说全:照题面,int 够;而写 long long 的好处是「不依赖那句承诺」 (和第 24 章 P5365 那条「截断让代码不依赖那段推理」是同一个道理)。

5★ 规模

链上 2N = 200 个标记,区间最长 N 颗珠子 ⇒ 转移 661 650
f202 × 202long long 318 KB

6度量程序、生成器和参照物

p1063Count.cpp度量程序(本页所有数字都出自它)
p1063Gen.cpp数据生成器
p1063Brute.cpp参照物:环上枚举聚合顺序(300 轮不一致 0 轮)

7一页纸

★★ 关键的一步(一) 区间的端点是「标记」不是「珠子」 ⇒ 转移是 f[l][k] + f[k][r] + a[l]a[k]a[r]
★★ 关键的一步(二) 破环成链(和 P1880 一模一样)
规模 转移 661 650 次,f 表 318 KB
没破环 恒 ≤ 正解(300/300),被抓 251;★ 样例挡住
★★ 写成 f[k+1][r] n ≤ 100300/300 被抓,⚠ 而官方样例放过它(样例 n = 4 太小)
★★★ 「E ≤ 2.1 × 10⁹ 不是推出来的(推出来是 9.9 × 10¹⁰,大 46 倍),是出题人挡掉的
⇒ 生成器要守输出侧的保证 顺手随机 300 轮里 60 轮越过它、54 轮越过 int;标记压到 300 才守得住
int 行不行 守得住那句保证时 0 / 300;守不住时 54 / 300(≡ 越过 int 的 54 轮)