题单 · 习题解析

洛谷 P1775 石子合并(弱化版)

★★★ 它就是本章正文那道题 —— 所以这一页量的是别的:「只能合并相邻两堆」这五个字值多少(去掉它就是合并果子,顶格只差 1.15%),以及两个贪心为什么一个恒 ≤ 一个恒 ≥

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

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

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

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

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

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

题目描述

设有 N (N ≤ 300) 堆石子排成一排,其编号为 1, 2, 3, …, N。 每堆石子有一定的质量 mᵢ (mᵢ ≤ 1000)。现在要将这 N 堆石子合并成为一堆。 每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和, 合并后与这两堆石子相邻的石子将和新堆相邻。 合并时由于选择的顺序不同,合并的总代价也不相同。 试找出一种合理的方法,使总的代价最小,并输出最小代价。

输入格式

第一行,一个整数 N

第二行,N 个整数 mᵢ

输出格式

输出文件仅一个整数,也就是最小代价。

输入输出样例

输入

4
2 5 3 1

输出

22

四堆 2 5 3 1。先合 (3,1) 得 4,再合 (5,4) 得 9,最后合 (2,9) 得 11 ⇒ 4 + 9 + 11 = 22

★ 而去掉「相邻」两个字(允许任意两堆合并)的答案是 20 —— 样例这一组就已经差 2 了。那五个字到底值多少,第 ② 步整节在量它。

1★ 先说清楚这一页的位置:它就是本章正文那道题

第 26 章正文从头到尾讲的就是这道题(直线版、只求最小值)。 所以「关键的一步」这一页不打算重讲一遍 —— 正文第 7、8 步已经把 「按区间长度从小到大」和「判据是依赖,不是那个写法」讲透了。

p1775.cpp★ 这一版就能 AC(和正文的 fast.cpp 是同一份)
// P1775 石子合并(弱化版)—— ★ 这一版就能 AC。
//
// ★★ 先说清楚这一页的位置:**这道题就是[第 26 章正文](/ch/26-interval-dp/)
// 那道题的原题**(直线版、只求最小值)。所以下面这份代码和正文的 `fast.cpp`
// **是同一个算法、同一份写法**,一个字都不用改:
//
// f[l][r] = min over k in [l, r-1] of ( f[l][k] + f[k+1][r] ) + (a[l] + … + a[r])
// ① 先枚举区间长度(从短到长)② 再枚举左端点 ③ 最后枚举断点
//
// ⇒ 题单那句「写完直接交,一遍就该过」是真的。
// ⚠ 而这一页要问的是另外三件事,**没有一件在 DP 上**:
// ① 「只能合并**相邻**两堆」这五个字值多少(去掉它就是[合并果子](/sol/p1090/));
// ② 少写一个前缀和会贵多少(⚠ 我的草稿在这儿猜错了);
// ③ 顶格答案有多大 —— 要不要 `long long`。
//
// 规模:`N ≤ 300` ⇒ 转移 `n(n²−1)/6` = **4 499 950** 次,眨眼就跑完。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (scanf("%d", &n) != 1 || n <= 0) return 0;
vector<long long> s(n + 1, 0); // 前缀和,1 基下标
for (int i = 1; i <= n; i++) {
long long x;
if (scanf("%lld", &x) != 1) return 0;
s[i] = s[i - 1] + x;
}
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 - 1 <= n; l++) { // ② 左端点
int r = l + len - 1;
long long best = LLONG_MAX;
for (int k = l; k < r; k++) // ③ 断点
best = min(best, f[l][k] + f[k + 1][r]);
f[l][r] = best + s[r] - s[l - 1]; // 最后一次合并:整段之和
}
printf("%lld\n", f[1][n]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

⇒ 题单那句「写完直接交,一遍就该过」是真的。

这一页要问的是另外三件事,没有一件在 DP 上:

「只能合并相邻两堆」这五个字值多少
少写一个前缀和会贵多少(⚠ 我的草稿在这儿猜错了)
顶格答案有多大 —— 要不要 long long

2★★★ 把「只能合并相邻两堆」这五个字称出重量

去掉这五个字,这道题就变成了另一道题 —— 而那道题我们已经写过一整页: 第 19 章 P1090 合并果子。那里的贪心(每次挑最小的两堆合并, 也就是哈夫曼)是有证明的最优解

★ 同一份输入,喂给两道题的正解

把 P1775 的样例 4 / 2 5 3 1 原样喂给 P1090 那一页的正解

P1775(只能合相邻 22
P1090(任意两堆 20

⇒ 「相邻」这两个字在样例上就值 2

p1090.cpp(第 19 章那一页的正解,原样拿来跑)去掉「相邻」之后的最优解
★★ 量一遍(各 300 轮,石子数 1~100)
堆数上限 9 20 60 300(题面顶格)
哈夫曼的答案 ≤ 正解 300 / 300 300 / 300 300 / 300 300 / 300
平均多少 3.64% 2.61% 1.72% 1.15%
最多低多少 24.88% 11.70% 3.56% 1.79%

★ 第一行是白送的推论:去掉一条约束,可行集只会变大 ⇒ 答案只会更小或相等。 ★ 而第二、三行是这一节真正的收获:「相邻」这条约束的分量,随堆数越来越小 (顶格只差 1.15%)—— 堆一多,最优的合并树本来就接近平衡,绕不绕远差别不大。

⚠ 所以拿哈夫曼交这道题会 WA,但看输出根本看不出来(顶格只小 1%)—— 和第 20 章 P1048 那个「偶尔错一点点」的贪心是同一种难查法。

★★ 顺带一件很值得记的事:这一页的两个贪心,方向正好相反
贪心 和正解的关系 300 轮里
哈夫曼(挑最小的两堆,不管相不相邻) 恒 ≤ 正解 300 / 300 ≤,272 轮不等
相邻里挑最小的一对(正文 greedy.cpp 恒 ≥ 正解 300 / 300 ≥,被抓 114 轮

两个都叫「先合最小的」,一个偏小一个偏大 —— 因为 前者放宽了题目的约束(算的是另一道更容易的题), 后者老老实实守着约束(给出的是一个合法但不最优的方案)。

⇒ ★★★ 「这个贪心的答案偏大还是偏小」不用跑就能判: 它给出的是一个可行方案 ⇒ 恒 ≥ 最优;它解的是放宽后的问题 ⇒ 恒 ≤ 最优。

greedy.cpp(正文那份:相邻里挑最小的一对)✗ 恒 ≥ 正解,300 轮被抓 114 次

3⚠⚠ 少写一个前缀和贵多少 —— 我的草稿在这儿猜错了

正解那一行 f[l][r] = best + s[r] - s[l-1] 用的是第 6 章的前缀和。 把它换成「现加一遍」会怎样?草稿里我写的是「从 O(n³) 掉到 O(n⁴)」。

p1775Slow.cpp每个区间现算一次区间和(它是对的)
// P1775:区间 DP 写对了,但**区间和是每次现算的** —— 它是对的,只是慢一个量级。
//
// 正解那一行 `f[l][r] = best + s[r] - s[l-1]` 用的是第 6 章的前缀和,`O(1)`。
// 这一版把它换成一个 `for` 循环现加一遍。
//
// ⚠⚠ 我的草稿在这儿写的是「复杂度从 `O(n³)` 掉到 `O(n⁴)`」—— **实测把它打回来了**:
// 区间只有 `O(n²)` 个,而这一句是**每个区间只算一次**,
// 所以它加起来是 `Σ len ≈ n³/6` —— 和 DP 的转移次数**同一个量级**。
// 顶格 `n = 300`:转移 4 499 950 次,这一版多做 4 544 800 次加法 —— **只贵 1.0 倍**。
//
// ★ 真要掉一个量级,得把它写进**最里面那层 `k` 循环**里(那才是 `O(n⁴)`,
// 顶格约 3.4 亿次)。⇒ **「少写一个前缀和」值多少,取决于你把它少写在哪一层。**
//
// ⚠ 它答案永远是对的 ⇒ **对拍一万轮也发现不了**([P5019](/sol/p5019/) 那条:
// 样例和对拍筛的都是「答案错」,对「答案对但跑不完」完全无能为力)。
#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 + 1, 0);
for (int i = 1; i <= n; i++) if (scanf("%lld", &a[i]) != 1) return 0;
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 - 1 <= n; l++) {
int r = l + len - 1;
long long best = LLONG_MAX;
for (int k = l; k < r; k++) best = min(best, f[l][k] + f[k + 1][r]);
long long sum = 0;
for (int i = l; i <= r; i++) sum += a[i]; // ✗ 每次现加一遍:多出来的那一层
f[l][r] = best + sum;
}
printf("%lld\n", f[1][n]);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 实测把那句话打回来了(顶格 n = 300)
次数 相对转移次数
DP 的转移 4 499 950 1.00
① 每个区间现算一次(上面这一版) 4 544 800 1.0 倍
② 把它写进最里层的 k 循环 679 492 450 151 倍

区间只有 O(n²),而写法 ① 是「每个区间只算一次」,加起来是 Σ len ≈ n³/6 —— 和 DP 的转移同一个量级。 ⇒ ★★★ 「少写一个前缀和」值多少,取决于你把它少写在哪一层。 只有写进最里面那层 k 循环,才真的掉一个量级。

⚠ 而这一版答案永远是对的 ⇒ 对拍一万轮也发现不了 (第 20 章 P5019 那条:样例和对拍筛的都是「答案错」)。

4★ 顶格答案有多大:一道三十秒的算术题

    每堆石子最多被「数」n − 1 次(每合并一次,它所在的那一堆就被计入一次代价)
    ⇒ 答案 ≤ 总质量 × (n − 1) = 300 × 1000 × 299 = 89 700 000
    int 的上限                                    = 2 147 483 647          ⇒ 余量 24 倍
顶格 n = 300、每堆都是 1000(最坏形状) 2 488 000
随机顶格 200 组里最大的一个 1 336 873
那个不用跑的上界 89 700 000

int 绰绰有余(正解用 long long 只是习惯,不是必须)。 ★ 注意那个上界松了 36 倍 —— 因为「每堆被数 n−1 次」是链式合并的最坏情形, 而最优解合出来的是一棵接近平衡的树,每堆只被数 log n 次左右。

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

p1775Count.cpp度量程序(本页所有数字都出自它)
gen.cpp(正文那份,四个档位的来历都写在注释里)数据生成器
brute.cpp(正文那份:枚举合并顺序,(n−1)! 条路径)参照物(300 轮不一致 0 轮)

6一页纸

它是什么 本章正文那道题的原题(直线版、只求最小)⇒ 正文的 fast.cpp 直接就能交
规模 N ≤ 300 ⇒ 转移 n(n²−1)/6 = 4 499 950
★★★ 「相邻」这五个字 去掉它就是合并果子(哈夫曼);哈夫曼恒 ≤ 正解,顶格平均只低 1.15% ⇒ 交上去 WA,但输出看着完全正常
★★ 两个贪心方向相反 放宽约束的那个恒 ≤,守着约束的那个恒 ≥ —— 不用跑就能判方向
⚠⚠ 草稿被打回来 「少写前缀和 ⇒ O(n⁴)」是错的:每区间算一次只贵 1.0 倍,写进 k 循环才是 151 倍
★ 要不要 long long 不用:上界 300 × 1000 × 299 = 8.97 × 10⁷int 余量 24 倍(顶格实测 2 488 000)