题单 · 习题解析

洛谷 P5019 [NOIP 2018 提高组] 铺设道路

★★★ 样例挡得住「答案错」,挡不住「跑不完」—— 分治那版答案永远对,却在单调数列上退化成 O(n²)(比较次数多 238.9 倍,而且倍数在涨)

⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

NOIP2018 提高组 D1T1

题目描述

春春是一名道路工程师,负责铺设一条长度为 n 的道路。

铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n 块连续的区域, 一开始,第 i 块区域下陷的深度为 dᵢ

春春每天可以选择一段连续区间 [L, R],填充这段区间中的每块区域,让其下陷深度减少 1。 在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0

春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0。

输入格式

输入文件包含两行,第一行包含一个整数 n,表示道路的长度。 第二行包含 n 个整数,相邻两数间用一个空格隔开,第 i 个整数为 dᵢ

输出格式

输出文件仅包含一个整数,即最少需要多少天才能完成任务。

数据规模与约定

  • 对于 30% 的数据,1 ≤ n ≤ 10
  • 对于 70% 的数据,1 ≤ n ≤ 1000
  • 对于 100% 的数据,1 ≤ n ≤ 1000000 ≤ dᵢ ≤ 10000

输入输出样例

输入

6
4 3 2 5 3 5

输出

9

样例解释给了一种最优方案,依次选择: [1,6][1,6][1,2][1,1][4,6][4,4][4,4][6,6][6,6] —— 共 9 天。

★ 这一页的主线:样例挡得住「答案错」,挡不住「跑不完」

第 20 章的题单给这道题写的是: 「贪心是对的,但你得说得出为什么。先写暴力对拍,再想证明。」 ⇒ 第 ② 步把那个证明写全(两半,缺一半都不算证完)。

而这一页真正的主角在第 ⑤ 步:这道题有第二种解法(分治),它的答案永远是对的, 复杂度却取决于数据形状 —— 随机数据上 O(n log n)单调数列上 O(n²)

⇒ 本书前面五道题连着量出「官方样例是个一测就死的过滤器」。 这一页给出它的补集:那个过滤器筛的是「答案错」, 对「答案对但跑不完」完全无能为力 —— 而对拍也一样。

1正解:一行公式

p5019.cpp★ 这一版就能 AC
// P5019 [NOIP 2018 提高组] 铺设道路 —— ★ 这一版就能 AC
//
// 题意:n 块区域,第 i 块下陷 d_i。每天可以选一段**连续区间**,让区间里每块的深度减 1,
// 前提是区间内每块的深度都不为 0。问最少几天能全部填平。
//
// ★ 关键一步:答案 = **所有「上升沿」的高度之和**
//
// ans = Σ max(0, d[i] − d[i−1]) (约定 d[0] = 0)
//
// 证明要两半,缺一半都不算证完(页面第 ② 步):
// · **下界**:一天只能选一段连续区间 ⇒ 一天最多让**一个**上升沿降低 1
// ⇒ 至少需要「上升沿总高度」那么多天;
// · **上界**:按这个数确实能填完(页面第 ② 步给了构造)。
//
// ⚠ 两处要算一算的地方:
// ① `d[0] = 0` 这个约定不能省 —— 第一块本身就是一个上升沿;
// ② 答案最大约 `10⁴ × n/2 = 5 × 10⁸`(相邻 0 / 10⁴ 交替),`int` 够用 ——
// ★ **算完确认够用,和没算过是两回事**。
//
// 复杂度 `O(n)`,读入 10⁵ 个数,随便怎么读都行。
// ★ 顺带:洛谷 P1969「积木大赛」和这道题是同一道题(问法反过来,答案一模一样)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
long long ans = 0, prev = 0; // ★ prev 从 0 开始 —— 这就是 d[0] = 0
for (int i = 0; i < n; i++) {
long long d;
cin >> d;
if (d > prev) ans += d - prev; // 只有「升上去」的那一截要单独花天数
prev = d;
}
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
    ans = Σ max(0, d[i] − d[i−1])        约定 d[0] = 0

拿样例走一遍(4 3 2 5 3 5,前面补一个 0):

    0 → 4   升 4     ans = 4
    4 → 3   降       ans = 4
    3 → 2   降       ans = 4
    2 → 5   升 3     ans = 7
    5 → 3   降       ans = 7
    3 → 5   升 2     ans = 9

9 天,和样例解释里那个方案对上了。

2★ 证明要两半 —— 缺一半都不算证完

下界:至少需要这么多天

一天只能选一段连续区间,区间里每块都减 1。 盯住任意一个「上升沿」—— 位置 id[i] > d[i−1],高度 h = d[i] − d[i−1]

一天之内,d[i]d[i−1] 要么减 1(ii−1 都在选中的区间里), 要么只有 d[i] 减 1(区间从 i 开始)—— 不可能只减 d[i−1](那样区间里就有 d[i] 断掉了)。 ⇒ 一天最多让一个上升沿的高度减少 1

⇒ 天数 ≥ Σ 所有上升沿的高度

上界:这么多天确实够

构造很直接:只要还有没填平的地方,就挑第一个还没填平的极大连续段,整段减 1。 这一天恰好让「第一个上升沿」降低 1,而且不会把别的上升沿变高。

⇒ 每天正好消耗 1,Σ 上升沿高度 天之后全部填平。

上下界撞在一起 ⇒ 那个公式就是答案。 而「一天一天地填」这个构造本身也能写成程序, 它是这一页的第三条路(第 ③ 步)。

3三条完全不同的路,算出同一个数

p5019Divide.cpp第二条路:分治
// P5019 的第二种解法:**分治** —— 它也是对的,放在这儿当参照物
//
// 想法:在区间 `[l, r]` 里找最矮的那一块(深度 m)。
// 整段先一起降 m 天(这 m 天每天都能选整个区间),剩下的被最矮那块切成若干独立小段,
// 各自递归。⇒ `solve(l, r) = m + Σ solve(每个子段)`。
//
// ★ 它和差分那一行**一个字都不共享** —— 所以它是个像样的参照物。
//
// ⚠⚠ 而它的复杂度**取决于数据形状**,这一点很值得量(页面第 ⑤ 步):
// · 随机数据上,最矮的那块大致把区间劈成两半 ⇒ `O(n log n)`;
// · **单调数列**上,最矮的永远在一头 ⇒ 每次只剥掉一块 ⇒ `O(n²)`,顶格 10¹⁰ 级别。
// ★ 而随机数据**看不出这件事** —— 这是[第 53 章](/ch/53-hld/)那条
// 「有一类 bug 只坏复杂度不坏答案」的又一次现场。
//
// ⚠ 递归深度也跟着形状走,所以这里写成显式栈,免得单调数据直接爆栈。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> d(n);
for (auto& x : d) cin >> x;
long long ans = 0;
// 栈里存 (l, r, base):这一段还没被填掉的部分,已经整体降过 base
vector<array<long long, 3>> st;
st.push_back({0, (long long)n - 1, 0});
while (!st.empty()) {
auto [l, r, base] = st.back();
st.pop_back();
if (l > r) continue;
long long m = LLONG_MAX;
for (long long i = l; i <= r; i++) m = min(m, d[i]);
ans += m - base; // 这一段整体再降 (m - base) 天
long long start = l;
for (long long i = l; i <= r; i++) {
if (d[i] == m) { // 最矮的那些块把区间切开
if (start <= i - 1) st.push_back({start, i - 1, m});
start = i + 1;
}
}
if (start <= r) st.push_back({start, r, m});
}
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

分治的想法和差分公式一个字都不共享:在 [l, r] 里找最矮的那块(深度 m), 整段先一起降 m 天,剩下的被最矮那块切成若干独立小段,各自递归。

300 组随机数据
差分公式 vs 分治 不一致 0 组
差分公式 vs 一天一天地填(第 ② 步那个构造) 不一致 0 组

4★★ 两个错法:说清楚它们「算了什么」

p5019NoMax.cpp错法一:忘了 max(0, ·)
// P5019 错法一:忘了 `max(0, ·)`,直接把差累加起来
//
// ★★ 说清楚它**算了什么**比说它「错了」有用得多 —— 这一版算的是一个望远镜求和:
//
// Σ (d[i] − d[i−1]) = d[n] − d[0] = d[n]
//
// 也就是说:**它永远输出最后一块的深度**(页面第 ④ 步实测逐组相等)。
// ⇒ 这个结论一出来,它的所有表现都是白送的推论:
// · 数列**单调不减**时它是对的(那时候确实没有下降沿);
// · 只要出现过一次下降,它就偏小。
//
// ★ 而官方样例 `4 3 2 5 3 5` 最后一块是 5 ⇒ 它输出 5,答案是 9 ⇒ **样例挡住了它**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
long long ans = 0, prev = 0;
for (int i = 0; i < n; i++) {
long long d;
cin >> d;
ans += d - prev; // ← 少了 max(0, ·)
prev = d;
}
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

max(0, ·) 丢掉之后,累加的是一个望远镜求和

    Σ (d[i] − d[i−1])  =  d[n] − d[0]  =  d[n]
p5019NoZero.cpp错法二:漏掉 d[0] = 0
// P5019 错法二:漏掉「`d[0] = 0`」这个约定 —— 从第二块才开始数上升沿
//
// 第一块本身就是从平地(0)升上来的,它那一截也要花天数。
// 漏掉之后,答案正好少了 `d[1]`。
//
// ★★ 而这个 bug 也能一句话说清楚它算了什么:**它算的是「答案 − 第一块的深度」**。
// ⇒ 只有当第一块是 0 的时候它才对。
// ★ 官方样例第一块是 4 ⇒ 它输出 5,答案 9 ⇒ **样例挡住它**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> d(n);
for (auto& x : d) cin >> x;
long long ans = 0;
for (int i = 1; i < n; i++) // ← 从 i = 1 开始,第一块那一截丢了
if (d[i] > d[i - 1]) ans += d[i] - d[i - 1];
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

第一块本身就是从平地升上来的,漏掉之后答案正好少了 d[1]

随机 300 轮
「忘了 max」那版 ≡ 最后一块的深度 300 / 300 逐组相等
「漏掉 d[0]=0」那版 ≡ 答案 − 第一块的深度 300 / 300 逐组相等
它们各自被抓的轮数 261 / 300 和 276 / 300
★ 说清楚「算了什么」之后,所有表现都是白送的推论
  • 「忘了 max」那版恒等于 d[n]数列单调不减时它是对的(那时候确实没有下降沿), 只要出现过一次下降就偏小;
  • 「漏掉 d[0]」那版恒等于「答案 − d[1]」⇒ 只有第一块是 0 时它才对

⇒ 这是本书反复用的那个动作(P1332 那页说得最清楚, 同一天的 P1048 又用了一次): 先说清楚 bug 算的是什么,再说它错在哪。

★ 官方样例把两个都挡住了(最后一块是 5、第一块是 4,两版都输出 5,而答案 9)—— 因为它们都属于「几乎每一组都错」的那一类。

5★★★ 而真正会挂人的第三个问题,样例和对拍都看不见

分治那一版答案永远是对的(上面 300 组验过)。可它的复杂度取决于数据长什么样:

  • 随机数据:最矮的那块大致把区间劈成两半 ⇒ O(n log n)
  • 单调数列:最矮的永远在一头 ⇒ 每次只剥掉一块 ⇒ O(n²)

用一把机器无关的尺子量(数「找最矮那块」一共比了多少次):

n 1 000 4 000 16 000
随机数据 12 226 58 966 267 688
单调数列 474 513 6 658 705 63 939 885
倍数 38.8 112.9 238.9
★★★ 关键是那个倍数在往上走 —— 这就是 O(n²) 的签名

n 每翻 4 倍:

    随机数据的比较次数   ×4.8   ×4.5     <- 接近 n log n
    单调数列的比较次数   ×14.0  ×9.6     <- 接近 n^2 的 ×16

⇒ 顶格 n = 10⁵ 时,单调数列上大约要比 5 × 10⁹ 次 —— 一秒钟的时限下必挂。

★★ 而样例看不见它(答案对)、对拍也看不见它(答案对)。 本书前面五道题连着量出「官方样例是个一测就死的过滤器」, 这一页正好给出它的边界:那个过滤器筛的是「答案错」。 「答案对但跑不完」要靠另一件事发现 —— 数一数次数,而且要造对形状第 53 章那条「有一类 bug 只坏复杂度不坏答案」的又一次现场)。

⇒ 所以这道题的正解就写那一行公式O(n)、没有形状依赖、没有递归深度问题。 分治留在这一页,是为了当参照物,也是为了把上面这件事量出来。

6★ 顺手算一笔:int 够不够

最坏形状是 0, 10⁴, 0, 10⁴, … 交替 —— 每一个 10⁴ 都是一个完整的上升沿:

顶格最坏(n = 10⁵)的答案 500 000 000
2³¹ 2 147 483 648
int 够用(余量 4.3 倍)

★ 这一笔仍然要算 —— 算完确认够用,和没算过是两回事第 6 章 P2367 那页立的规矩)。

7度量程序和生成器

p5019Count.cpp度量程序
// P5019 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p5019Count` 人看的版本
// `./p5019Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 五段:
// ① ★ 三条完全不同的路算出同一个数:差分公式 / 分治 / 一天一天地填;
// ② ★★ 两个错法**算了什么**(不是「错了」):一个恒等于最后一块的深度,
// 一个恒等于「答案 − 第一块的深度」;
// ③ ★★★ 分治那版的复杂度**取决于数据形状** —— 用「比较次数」这把机器无关的尺子量;
// ④ ★ 顶格答案有多大(`int` 到底够不够,算一遍);
// ⑤ ★ 两个错法在随机数据上的抓获率。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
static bool CSV = false;
static void row(const char* key, const vector<ll>& v) {
if (!CSV) return;
printf("%s", key);
for (ll x : v) printf(",%lld", x);
printf("\n");
}
/** 正解:上升沿之和 */
static ll byDiff(const vector<ll>& d) {
ll ans = 0, prev = 0;
for (ll x : d) { if (x > prev) ans += x - prev; prev = x; }
return ans;
}
/** 错法一:忘了 max(0, ·) */
static ll noMax(const vector<ll>& d) {
ll ans = 0, prev = 0;
for (ll x : d) { ans += x - prev; prev = x; }
return ans;
}
/** 错法二:漏掉 d[0] = 0 */
static ll noZero(const vector<ll>& d) {
ll ans = 0;
for (size_t i = 1; i < d.size(); i++) if (d[i] > d[i - 1]) ans += d[i] - d[i - 1];
return ans;
}
/** 第二条路:分治。cmp 记录「找最矮那块」一共比了多少次 —— 机器无关的尺子 */
static ll byDivide(const vector<ll>& d, ll* cmp = nullptr) {
ll ans = 0;
if (cmp) *cmp = 0;
vector<array<ll, 3>> st{{0, (ll)d.size() - 1, 0}};
while (!st.empty()) {
auto [l, r, base] = st.back();
st.pop_back();
if (l > r) continue;
ll m = LLONG_MAX;
for (ll i = l; i <= r; i++) { m = min(m, d[i]); if (cmp) (*cmp)++; }
ans += m - base;
ll start = l;
for (ll i = l; i <= r; i++)
if (d[i] == m) { if (start <= i - 1) st.push_back({start, i - 1, m}); start = i + 1; }
if (start <= r) st.push_back({start, r, m});
}
return ans;
}
/** 第三条路:一天一天地填(每天挑第一个还没填平的极大连续段)—— 只在小数据上用 */
static ll bySimulate(vector<ll> d) {
ll days = 0;
while (true) {
int i = 0, n = d.size();
while (i < n && d[i] == 0) i++;
if (i == n) break;
int j = i;
while (j < n && d[j] > 0) j++;
for (int k = i; k < j; k++) d[k]--;
days++;
}
return days;
}
static vector<ll> gen(mt19937& rng, int n, int dHi, int shape) {
vector<ll> d(n);
for (int i = 0; i < n; i++) d[i] = (ll)(rng() % (unsigned)(dHi + 1));
if (shape == 1) sort(d.begin(), d.end());
else if (shape == 2) sort(d.rbegin(), d.rend());
return d;
}
int main(int argc, char** argv) {
CSV = argc > 1 && string(argv[1]) == "csv";
/* ① 三条路一致 */
{
mt19937 rng(20260829u);
int groups = 0, badDiv = 0, badSim = 0;
for (int rep = 0; rep < 300; rep++, groups++) {
vector<ll> d = gen(rng, (int)(rng() % 12) + 1, 10, 0);
ll ok = byDiff(d);
if (byDivide(d) != ok) badDiv++;
if (bySimulate(d) != ok) badSim++;
}
if (!CSV) printf("① %d 组:差分公式 vs 分治不一致 %d 组;差分公式 vs 一天一天地填不一致 %d 组\n",
groups, badDiv, badSim);
row("three", {groups, badDiv, badSim});
}
/* ② 两个错法「算了什么」 */
{
mt19937 rng(5019u);
int rounds = 300, eqLast = 0, eqMinusFirst = 0;
for (int r = 0; r < rounds; r++) {
vector<ll> d = gen(rng, (int)(rng() % 30) + 1, 20, 0);
if (noMax(d) == d.back()) eqLast++;
if (noZero(d) == byDiff(d) - d[0]) eqMinusFirst++;
}
if (!CSV) printf("② %d 轮:「忘了 max」那版 ≡ 最后一块的深度 %d 轮;"
"「漏掉 d[0]=0」那版 ≡ 答案 − 第一块深度 %d 轮\n", rounds, eqLast, eqMinusFirst);
row("what", {rounds, eqLast, eqMinusFirst});
}
/* ③ ★★★ 分治的复杂度取决于形状 —— 数比较次数,不看秒表 */
{
const int NS[] = {1000, 4000, 16000};
vector<ll> out;
for (int n : NS) {
mt19937 rng(n * 7919u + 7u);
ll cRand = 0, cSorted = 0;
{
vector<ll> d = gen(rng, n, 10000, 0);
byDivide(d, &cRand);
}
{
vector<ll> d = gen(rng, n, 10000, 1); // 单调不减
byDivide(d, &cSorted);
}
out.push_back(cRand); out.push_back(cSorted);
if (!CSV) printf("③ n = %5d:分治在随机数据上比了 %lld 次,在单调数列上比了 %lld 次"
"(差 %.1f 倍)\n", n, cRand, cSorted, (double)cSorted / cRand);
}
row("shape", out);
}
/* ④ 顶格答案有多大 */
{
int n = 100000;
vector<ll> d(n);
for (int i = 0; i < n; i++) d[i] = (i % 2) ? 10000 : 0; // 0 / 10^4 交替 —— 最坏形状
ll ans = byDiff(d);
if (!CSV) printf("④ 顶格最坏(n = 10⁵,0 / 10⁴ 交替):答案 %lld,而 2³¹ = 2147483648 "
"⇒ int %s\n", ans, ans < 2147483648LL ? "够用" : "不够");
row("worst", {ans, 2147483648LL, ans < 2147483648LL ? 1 : 0});
}
/* ⑤ 两个错法的抓获率 */
{
mt19937 rng(31337u);
int rounds = 300, a = 0, b = 0;
for (int r = 0; r < rounds; r++) {
vector<ll> d = gen(rng, (int)(rng() % 12) + 1, 10, 0);
ll ok = byDiff(d);
if (noMax(d) != ok) a++;
if (noZero(d) != ok) b++;
}
if (!CSV) printf("⑤ %d 轮随机:「忘了 max」被抓 %d 次,「漏掉 d[0]=0」被抓 %d 次\n", rounds, a, b);
row("catch", {rounds, a, b});
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p5019Gen.cpp数据生成器
// P5019 对拍生成器:`./p5019Gen <seed> [n 上限] [d 上限] [形状]`
// 形状:0 = 随机(默认) 1 = 单调不减 2 = 单调不增
//
// ★ 「形状」这个旋钮不是为了抓答案错的 bug —— 两个答案错法在随机数据上就被抓得干干净净。
// 它是为了**量分治那版的复杂度**(页面第 ⑤ 步):
// 随机数据上最矮的那块大致把区间劈成两半 ⇒ `O(n log n)`;
// **单调数列上最矮的永远在一头 ⇒ 每次只剥掉一块 ⇒ `O(n²)`**。
// ⚠ 而这件事**答案是对的**,任何比对答案的对拍都看不见它。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int nHi = argc > 2 ? atoi(argv[2]) : 12;
int dHi = argc > 3 ? atoi(argv[3]) : 10;
int shape = argc > 4 ? atoi(argv[4]) : 0;
nHi = max(1, min(100000, nHi));
dHi = max(0, min(10000, dHi));
mt19937 rng(seed * 2654435761u + 19u);
int n = (int)(rng() % (unsigned)nHi) + 1;
vector<int> d(n);
for (int i = 0; i < n; i++) d[i] = (int)(rng() % (unsigned)(dHi + 1));
if (shape == 1) sort(d.begin(), d.end());
else if (shape == 2) sort(d.rbegin(), d.rend());
printf("%d\n", n);
for (int i = 0; i < n; i++) printf("%d%c", d[i], i + 1 == n ? '\n' : ' ');
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

⚠ 生成器的第四个参数是形状(0 随机 / 1 单调不减 / 2 单调不增)。 它不是为了抓答案错的 bug —— 那两个在随机数据上就被抓得干干净净; 它是为了量分治那版的复杂度

8一页纸

关键的一步 ans = Σ max(0, d[i] − d[i−1]),约定 d[0] = 0O(n) 一行)
哪一版能 AC p5019.cpp
证明 两半:下界(一天最多让一个上升沿降 1)+ 上界(每天填第一个极大段)
三条路 差分公式 / 分治 / 一天一天地填 —— 300 组各不一致 0 组
★★ 两个错法 「忘了 max≡ 最后一块的深度、「漏掉 d[0]=0≡ 答案 − 第一块深度
(各 300 / 300 逐组相等)⇒ 它们的所有表现都是白送的推论
★★★ 这一页的主线 样例挡得住「答案错」,挡不住「跑不完」 ——
分治答案永远对,而它在单调数列上退化成 O(n²)
比较次数比随机数据多 38.8 → 112.9 → 238.9 倍(倍数在涨 = O(n²) 的签名)
顺手一笔 顶格最坏答案 5 × 10⁸int 够用(余量 4.3 倍)—— 但要算过才能这么说
同一道题 洛谷 P1969「积木大赛」问法反过来,答案一模一样