题单 · 习题解析

洛谷 P2678 [NOIP 2015 提高组] 跳石头

「最小值最大」是镜像,mid 要上取整;★ 那个精确的 0 是我把档位的方向造反了

原题:洛谷 P2678出自 第 9 章 二分答案 的题单题面本地存档:2026-08-27
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

NOIP2015 Day2 T1。

题目描述

一年一度的“跳石头”比赛又要开始了!

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石 作为比赛起点终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。 在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。 由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

输入格式

第一行包含三个整数 L, N, M,分别表示起点到终点的距离,起点和终点之间的岩石数, 以及组委会至多移走的岩石数。保证 L ≥ 1N ≥ M ≥ 0

接下来 N 行,每行一个整数,第 i 行的整数 Dᵢ0 < Dᵢ < L),表示第 i 块岩石 与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式

一个整数,即最短跳跃距离的最大值。

说明 / 提示

对于 20% 的数据,0 ≤ M ≤ N ≤ 10。对于 50% 的数据,0 ≤ M ≤ N ≤ 100

对于 100% 的数据,0 ≤ M ≤ N ≤ 500001 ≤ L ≤ 10⁹

输入输出样例

输入

25 5 2
2
11
14
17
21

输出

4

将与起点距离为 214 的两个岩石移走后,最短的跳跃距离为 4 (从与起点距离 17 的岩石跳到距离 21 的岩石,或者从距离 21 的岩石跳到终点)。

★ 注意样例解释最后那半句:最短的那一跳可能就是「到终点」那一跳。 第 ③ 步那个 WA 就是把它忘了。

1★ 它和本章正文那道题是镜像的

★★ 「最大值最小」↔「最小值最大」:方向一反,二分的两条分支跟着反

第 9 章正文那道 P1182 求的是「每段和的最大值最小」, 这道题求的是「最短跳跃距离的最小值最大」。判定函数的单调方向正好相反:

P1182   ok(x) = 「每段都不超过 x」能切成 <= M 段吗
        x 越大越容易  ->  ✗ ✗ ✗ ✓ ✓ ✓   要的是第一个 ✓

P2678   ok(x) = 「每一跳都 >= x」要移走的石头 <= M 块吗
        x 越大越难    ->  ✓ ✓ ✓ ✗ ✗ ✗   要的是最后一个 ✓

于是模板的两条分支也跟着反:

if (ok(mid)) lo = mid;      // 可行 -> 答案在右边,mid 自己也算候选
else         hi = mid - 1;  // 不可行 -> mid 淘汰

这时 mid 必须上取整mid = lo + (hi - lo + 1) / 2。 否则 lohi 只差 1 时 mid == lo,走 lo = mid 那一支区间一点没缩 —— 死循环。(第 8 章第 ⑥ 步那个坑的镜像版: 一边是 l = mid 死循环,这边是 lo = mid 死循环,救法都是把不对称的那个 +1 放对地方。)

2★ 这一版就已经能 AC 了

p2678.cpp★ 这一版就能 AC
// P2678 跳石头 —— ★ 这一版就能 AC
//
// 「移走至多 M 块石头,使**最短跳跃距离尽可能长**」——「最小值最大」,
// 和本章正文那道「最大值最小」(P1182)是**镜像**的:
//
// P1182:ok(x) = 「每段不超过 x」能切成 <= M 段吗 x 越大越容易 ⇒ 找第一个 ✓
// P2678:ok(x) = 「每一跳都 >= x」要移走 <= M 块吗 x 越大越难 ⇒ 找最后一个 ✓
//
// ⚠ 方向反了 ⇒ 二分的两条分支也跟着反:
// 可行 → lo = mid(答案在右边,mid 自己也算) 不可行 → hi = mid − 1
// ★ 这时 mid 必须**上取整** `mid = lo + (hi - lo + 1) / 2`,否则 lo == hi−1 时死循环
// (第 8 章第 ⑥ 步那个坑的镜像版)。
//
// ok(x) 的贪心:从起点往右扫,够不到 x 就把这块移走;
// ⚠ **最后那一跳到终点也得数** —— 忘了它就是最常见的那个 WA(见 p2678NoEnd.cpp)。
//
// 上下界:lo = 0(M = N 时把石头全移走,答案是 L;但下界取 0 最稳),hi = **L**
// —— ⚠ 不是 max(D_i):石头全移走之后那一跳的长度就是 L(见 p2678HiMax.cpp)。
#include <bits/stdc++.h>
using namespace std;
static int n, m;
static long long L;
static vector<long long> d;
/** 每一跳都 >= x 的话,至少要移走几块 */
static int need(long long x) {
int cnt = 0;
long long last = 0;
for (int i = 0; i < n; i++) {
if (d[i] - last < x) cnt++; // 够不到 ⇒ 这块移走
else last = d[i];
}
if (L - last < x) cnt++; // ★ 终点那一跳也得够
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> L >> n >> m)) return 0;
d.assign(n, 0);
for (int i = 0; i < n; i++) cin >> d[i];
long long lo = 0, hi = L;
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2; // ★ 上取整
if (need(mid) <= m) lo = mid; // 可行 ⇒ 还能更长
else hi = mid - 1;
}
cout << lo << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

判定用贪心:从起点往右扫,够不到 x 的石头就移走。 顶格数据(L = 10⁹N = 5×10⁴M = 2.5×10⁴)实测:约 5 毫秒 / 4.4 MB

3⚠ 最常见的那个 WA:忘了终点那一跳

p2678NoEnd.cpp⚠ 会 WA 的
// ⚠ 最常见的那个 WA:ok() 里忘了**终点那一跳**
//
// if (L - last < x) cnt++; ← 删掉了
//
// 为什么容易忘:题面说的是「起点 → 石头 → …… → 终点」,
// 而循环是照着**石头**写的 —— 终点不是石头,它不在那个数组里。
// ⇒ **凡是「最后一段 / 最后一跳」不在数组里的题,都要单独补一句。**
//
// ★ 它错的方向是**偏大**:少数了一次「要移走的石头」,于是某些其实不可行的 x
// 被判成可行,二分往右多走了一段。
#include <bits/stdc++.h>
using namespace std;
static int n, m;
static long long L;
static vector<long long> d;
static int need(long long x) {
int cnt = 0;
long long last = 0;
for (int i = 0; i < n; i++) {
if (d[i] - last < x) cnt++;
else last = d[i];
}
return cnt; // ⚠ 少了终点那一跳
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> L >> n >> m)) return 0;
d.assign(n, 0);
for (int i = 0; i < n; i++) cin >> d[i];
long long lo = 0, hi = L;
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2;
if (need(mid) <= m) lo = mid;
else hi = mid - 1;
}
cout << lo << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 它在样例上是对的 —— 这正是它难缠的地方

样例它照样输出 4。因为最后一块石头(21)到终点(25)的距离正好是 4, 够得着,数不数它都一样。

为什么容易忘:循环是照着石头数组写的,而终点不是石头,它不在数组里。

凡是「最后一段 / 最后一跳」不在数组里的题,都要单独补一句。 (前缀和那一章的 d[y+1]、区间那一章的「最后一个右端点」,都是同一件事。)

4另外两个 WA:一个上界,一个等号

p2678HiMax.cpp⚠ 上界写成 max(Dᵢ)
// ⚠ 只改一处:上界写成 max(D_i),不是 L
//
// long long hi = d[n-1]; ← 本来是 L
//
// 想法是「最长的一跳总不会超过最远那块石头吧」—— ⚠ 会的:
// 把石头**全部**移走(M = N 时允许)之后,唯一的一跳就是起点直接到终点,长度 **L**,
// 而 L > D_i 对每块石头都成立(题面:`0 < D_i < L`)。
//
// ⇒ 这个错和 p1182LoAvg.cpp 正好凑成一对:
// **下界只要 <= 答案就安全,上界只要 >= 答案就安全** ——
// 出事的从来不是「松」,是「没包住」。
#include <bits/stdc++.h>
using namespace std;
static int n, m;
static long long L;
static vector<long long> d;
static int need(long long x) {
int cnt = 0;
long long last = 0;
for (int i = 0; i < n; i++) {
if (d[i] - last < x) cnt++;
else last = d[i];
}
if (L - last < x) cnt++;
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> L >> n >> m)) return 0;
d.assign(n, 0);
for (int i = 0; i < n; i++) cin >> d[i];
long long lo = 0, hi = n ? d[n - 1] : L; // ⚠ 就是这一行
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2;
if (need(mid) <= m) lo = mid;
else hi = mid - 1;
}
cout << lo << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

上界写成「最远那块石头」——⚠ 会漏:M = N 时石头全部移走, 唯一的一跳就是起点直接到终点,长度 L,而题面保证 Dᵢ < L。 ⇒ 又是「没包住」,和 P1182 那页第 ④ 步说的是同一件事。

p2678Lt.cpp⚠ 少了个等号
// ⚠ 只改一处:判定写成 `need(mid) < m`,少了个等号
//
// if (need(mid) < m) lo = mid; ← 本来是 <= m
//
// 题面:「**至多**移走 M 块」—— 正好移走 M 块是**允许**的。
// 少个等号就等于把「正好用满预算」的方案全部作废,答案偏小。
//
// ⚠ 这里原来写的是「它只在『最优方案恰好要移走 M 块』时才现形,随机数据抓不到」——
// **实测打脸:随机档 400 轮就抓到 318 次。**
// 道理很直白:`m = 0` 时 `need(mid) < 0` 永远不成立,它直接输出 0;
// 而 `m > 0` 时只要最优方案用满了预算就现形,这在小数据里很常见。
// ⇒ **「这个 bug 很难抓」也是一句要先量再说的话**,别拿直觉当结论。
#include <bits/stdc++.h>
using namespace std;
static int n, m;
static long long L;
static vector<long long> d;
static int need(long long x) {
int cnt = 0;
long long last = 0;
for (int i = 0; i < n; i++) {
if (d[i] - last < x) cnt++;
else last = d[i];
}
if (L - last < x) cnt++;
return cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> L >> n >> m)) return 0;
d.assign(n, 0);
for (int i = 0; i < n; i++) cin >> d[i];
long long lo = 0, hi = L;
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2;
if (need(mid) < m) lo = mid; // ⚠ 少了等号
else hi = mid - 1;
}
cout << lo << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

need(mid) < m 少了个等号 —— 题面是「至多移走 M 块」,正好移走 M 块是允许的。 ★ 这一版样例就挂(输出 3,正确答案 4)。

5★★★ 对拍:四个档位,和一个「造反了」的档位

参照物用枚举「移走哪几块」的暴力 —— 它和二分没有任何共用逻辑 (一个枚举方案,一个枚举答案),两边一起错的概率极低:

p2678Brute.cpp对拍参照物(2ⁿ)
// P2678 的暴力:枚举「移走哪几块」,取最短跳跃距离的最大值
//
// 石头只有 n 块,每块「移 / 不移」⇒ 2ⁿ 种方案;对每种方案算一遍最短跳。
// ⇒ O(2ⁿ × n),n = 20 就要一千万次,**满数据 n = 5×10⁴ 想都别想**。
//
// ★ 它的用处只有一个:**当对拍的参照物**。
// 它和二分那版**没有任何共用逻辑** —— 一个在枚举方案,一个在枚举答案,
// 两边一起错的概率极低(第 7 章 P1147 那条:验算要走一条无关的路)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long L;
int n, m;
if (!(cin >> L >> n >> m)) return 0;
vector<long long> d(n);
for (int i = 0; i < n; i++) cin >> d[i];
long long best = 0;
for (int mask = 0; mask < (1 << n); mask++) {
if (__builtin_popcount((unsigned)mask) > m) continue; // 至多移走 m 块
long long last = 0, mn = LLONG_MAX;
for (int i = 0; i < n; i++) {
if (mask >> i & 1) continue; // 这块被移走了
mn = min(mn, d[i] - last);
last = d[i];
}
mn = min(mn, L - last); // 终点那一跳
best = max(best, mn);
}
cout << best << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p2678Gen.cpp生成器:四个档位
// 数据生成器(P2678 对拍用):`./p2678Gen <seed> [level]`
//
// level 0(默认)随机小数据:n <= 10、L <= 40、m ∈ [0, n]
// level 1 ★ **石头挤在终点那一侧**:位置集中在 [3L/4, L−1] —— 逼出「终点那一跳」。
// ⚠ 这个档位一开始造反了:第一版挤在 **[1, L/4]**(左边),
// 结果对 p2678NoEnd 是**精确的 0**(400 轮一次没抓到)——
// 石头挤在左边,最后一块离终点**很远**,`L − last` 绰绰有余,
// 那句话删不删都一样。**要让最后一跳变短,石头就得挤在右边。**
// level 2 **预算刚好用光**:先随机造,再把 m 调成「最优解正好要移走的块数」
// level 3 ⚠ **对照档:石头挤在起点侧**([1, L/4])—— 故意留着的**反面教材**。
// 它是「level 1 造反了的那一版」,对 p2678NoEnd 是**精确的 0**。
// 留着它是为了把「方向搞反的档位长什么样」钉成一条断言:
// **看到一个精确的 0,先问「我这一档到底逼紧了哪一头」。**
//
// ⚠ n <= 10 是为了让 p2678Brute 的 2ⁿ 跑得动(它是对拍的参照物)。
#include <bits/stdc++.h>
using namespace std;
static mt19937 rng;
static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
/** 至多移走 m 块时的答案(照 p2678Brute 那套枚举,生成器自己也要会算) */
static long long best(long long L, vector<long long>& d, int m) {
int n = (int)d.size();
long long b = 0;
for (int mask = 0; mask < (1 << n); mask++) {
if (__builtin_popcount((unsigned)mask) > m) continue;
long long last = 0, mn = LLONG_MAX;
for (int i = 0; i < n; i++) { if (mask >> i & 1) continue; mn = min(mn, d[i] - last); last = d[i]; }
b = max(b, min(mn, L - last));
}
return b;
}
int main(int argc, char** argv) {
rng.seed((argc > 1) ? (unsigned)atoi(argv[1]) : 1);
int level = (argc > 2) ? atoi(argv[2]) : 0;
int n = ri(1, 10);
long long L = ri(n + 1, 40);
set<long long> pos;
while ((int)pos.size() < n) {
long long lo = (level == 1) ? max<long long>(1, L * 3 / 4) : 1;
long long hi = (level == 3) ? max<long long>(1, L / 4) : L - 1;
if (lo > hi) lo = 1;
pos.insert(ri((int)lo, (int)hi));
if ((int)pos.size() < n && (int)(hi - lo + 1) < n) n = (int)(hi - lo + 1); // 位置不够放,缩 n
}
vector<long long> d(pos.begin(), pos.end());
n = (int)d.size();
int m;
if (level == 2) {
/* 预算刚好用光:答案在 m 和 m−1 之间真的会变,说明第 m 块非移不可 */
m = ri(1, n);
while (m > 1 && best(L, d, m) == best(L, d, m - 1)) m--;
} else {
m = ri(0, n);
}
printf("%lld %d %d\n", L, n, m);
for (int i = 0; i < n; i++) printf("%lld\n", d[i]);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

每档 400 轮,和暴力答案不一致的轮数

档位 NoEnd 忘终点 HiMax 上界 Lt 少等号 正解 ≡ 暴力
level 0 随机 151 85 318 400 / 400
level 1 石头挤在终点 155 114 318 400 / 400
level 2 预算刚好用光 138 120 356 400 / 400
level 3 ⚠ 石头挤在起点 ★★★ 0 111 310 400 / 400
★★★ 那个 0 是我把档位造反了 —— 而且它是我第一版真写出来的东西

level 3 不是「凑数的对照」,它就是 level 1 的第一版

当时的想法很顺:「NoEnd 忘的是终点那一跳,那就把石头挤在一头,让贪心难受一点」—— 于是写了「位置集中在 [1, L/4]」。跑出来是精确的 0,400 轮一次没抓到。

回头看一眼就明白了:石头全挤在左边,最后一块离终点很远L − last 大得不得了,那句判断删不删结果都一样要让最后一跳变短,石头就得挤在右边。 改成 [3L/4, L−1] 之后是 155 / 400。

⇒ 这一条比那三列数字都值钱:看到一个精确的 0,先别急着说「这个 bug 很难抓」—— 先问「我这一档到底逼紧了哪一头」。 方向搞反的档位,看起来和「造对了但没抓到」一模一样。 ★ 所以那个造反了的档位留在生成器里level 3),并且把这个 0 钉成了一条断言。

⚠ 顺带一条:「这个 bug 很难抓」也得先量再说

p2678Lt.cpp(少个等号)的注释第一版写的是 「它只在最优方案恰好要移走 M 块时才现形,随机数据抓不到」—— 实测随机档 400 轮抓到 318 次。

道理很直白:M = 0need(mid) < 0 永远不成立,它直接输出 0M > 0 时只要最优方案用满了预算就现形,小数据里这太常见了。

⇒ 又一次:别拿直觉当结论,尤其是「这个很难抓」这种听起来很谦虚的判断。

6换一把尺子:两种暴力慢在完全不同的地方

p2678Count.cpp数次数
// 换一把尺子:三条路各要做多少次
//
// 用法:./p2678Count <L> <n> <m> 人话版
// ./p2678Count <L> <n> <m> csv 只打 `键,值`,给 check:viz 用
//
// 这道题的三条路,代价的**来源完全不同**:
// ① 枚举「移走哪几块」(p2678Brute 那一版):2ⁿ 种方案 —— 由 **n** 决定
// ② 枚举答案,从 L 往下一个个试:最多 L + 1 次 —— 由 **L** 决定,和 n 无关
// ③ 二分答案:⌊log2(L)⌋ + 1 次判定,每次 O(n)
//
// ★ 值得单独记一句:① 和 ② **都是「暴力」,但它们慢在完全不同的地方**。
// n 小 L 大时 ① 反而更快,n 大 L 小时 ② 更快 ——
// 「暴力有多慢」这句话不写清楚**枚举的是什么**,就是没说。
#include <bits/stdc++.h>
using namespace std;
static int n, m;
static long long L;
static vector<long long> d;
static long long calls;
static int need(long long x) {
calls++;
int cnt = 0;
long long last = 0;
for (int i = 0; i < n; i++) {
if (d[i] - last < x) cnt++;
else last = d[i];
}
if (L - last < x) cnt++;
return cnt;
}
int main(int argc, char** argv) {
L = (argc > 1) ? atoll(argv[1]) : 1000000000LL;
n = (argc > 2) ? atoi(argv[2]) : 50000;
m = (argc > 3) ? atoi(argv[3]) : 25000;
bool csv = (argc > 4 && string(argv[4]) == "csv");
mt19937_64 rng(20260827ull);
set<long long> s;
while ((int)s.size() < n) s.insert((long long)(rng() % (unsigned long long)(L - 1)) + 1);
d.assign(s.begin(), s.end());
n = (int)d.size();
calls = 0;
long long lo = 0, hi = L;
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2;
if (need(mid) <= m) lo = mid; else hi = mid - 1;
}
long long binCalls = calls;
long long ans = lo;
long long enumAns = L - ans + 1; // ② 从 L 往下试到答案,要试几次
int lg = 0; for (long long x = L; x > 0; x >>= 1) lg++;
if (csv) {
printf("L,%lld\nn,%d\nm,%d\nanswer,%lld\n", L, n, m, ans);
printf("binCalls,%lld\nlog2L,%d\nenumFromL,%lld\n", binCalls, lg, enumAns);
printf("binOps,%lld\nenumOps,%lld\n", binCalls * n, enumAns * n);
printf("ratio,%lld\n", enumAns / max(1LL, binCalls));
return 0;
}
printf("L = %lld、n = %d、m = %d ⇒ 答案 = %lld\n\n", L, n, m, ans);
printf(" ② 枚举答案(从 L 往下一个个试) %12lld 次判定\n", enumAns);
printf(" ③ 二分答案 %12lld 次判定(⌊log2 L⌋+1 = %d)\n", binCalls, lg);
printf("\n 差 %lld 倍。每次判定都是 O(n) = %d 步,所以总步数差的也是这个倍数。\n",
enumAns / max(1LL, binCalls), n);
printf("\n ① 枚举「移走哪几块」是 2^%d 种方案 —— 它和 L 无关,只和 n 有关,\n", n);
printf(" n 到 %d 就已经是天文数字了。**两种暴力慢在完全不同的地方。**\n", n);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
做法 次数由谁决定 L = 10⁹n = 5×10⁴
① 枚举「移走哪几块」 n 2⁵⁰⁰⁰⁰ 种方案
② 枚举答案,从 L 往下试 L 999 979 874 次判定
③ 二分答案 log L 30 次判定
★★ 「暴力有多慢」不写清楚「枚举的是什么」,就是没说

① 和 ② 都叫「暴力」,可它们的代价来源毫无关系nL 大时 ① 更快,nL 小时 ② 更快。

★ ① 的增长是实测的:n = 20 要 0.02 秒、n = 22 要 0.10 秒 —— 每加 2 就翻两番。 ② 和它比呢?L 涨一倍它就慢一倍,和 n 一点关系都没有。

⇒ 说「这题暴力过不了」之前,先说清楚你说的是哪个暴力 —— 这道题的两个暴力,一个死在 n 上,一个死在 L 上,而二分两头都躲开了

p2678GenBig.cpp顶格数据
// 顶格数据(P2678 计时用):`./p2678GenBig <L> <n> <m> [level]`
//
// level 0(默认)石头位置在 [1, L−1] 里随机取 n 个不重复的
// level 1 **等间距**:最短跳固定 —— 用来看「答案落在哪儿」而不是「跑多久」
//
// ⚠ 题面:0 <= M <= N <= 5×10⁴、1 <= L <= 10⁹、0 < D_i < L 且两两不同、按升序给出。
// ⇒ 位置必须**去重 + 排序**,否则造出来的不是这道题的数据。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
long long L = (argc > 1) ? atoll(argv[1]) : 1000000000LL;
int n = (argc > 2) ? atoi(argv[2]) : 50000;
int m = (argc > 3) ? atoi(argv[3]) : 25000;
int level = (argc > 4) ? atoi(argv[4]) : 0;
vector<long long> d;
if (level == 1) {
for (int i = 1; i <= n; i++) d.push_back(L * i / (n + 1));
} else {
mt19937_64 rng(20260827ull);
set<long long> s;
while ((int)s.size() < n) s.insert((long long)(rng() % (unsigned long long)(L - 1)) + 1);
d.assign(s.begin(), s.end());
}
sort(d.begin(), d.end());
d.erase(unique(d.begin(), d.end()), d.end());
n = (int)d.size();
if (m > n) m = n;
printf("%lld %d %d\n", L, n, m);
for (int i = 0; i < n; i++) printf("%lld\n", d[i]);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

7一张总表

版本 做法 对拍(1600 轮) 结果
p2678Brute 枚举移走哪几块 O(2ⁿ × n) 参照物 ✗ TLE(n = 22 就 0.1 秒)
p2678 二分答案 + 贪心 —— AC(5 毫秒)
p2678NoEnd ② 忘了终点那一跳 444 WA(样例抓不到)
p2678HiMax ② 上界写成 max(Dᵢ) 430 WA(样例抓不到)
p2678Lt ② 少了个等号 1302 WA(样例就挂)
这一页记住三句话
  1. ★★ 「最小值最大」是「最大值最小」的镜像:判定的单调方向反了, 二分的两条分支跟着反,而且 mid上取整,否则死循环。
  2. ★★★ 看到一个精确的 0,先问「我这一档逼紧了哪一头」。 本页那个 0 是把「石头挤在一头」挤错了方向 —— 而「方向搞反的档位」和「造对了但抓不到」在数字上长得一模一样。
  3. ★★ 「暴力有多慢」不写清楚枚举的是什么,就是没说。 这道题两个暴力,一个死在 n 上(2ⁿ),一个死在 L 上(10⁹ 次), 而二分把两头都躲开了。