题单 · 习题解析

洛谷 P1223 排队接水

★★★ 有一类题,弯路一步都不在算法里 —— 算法是第 19 章正文证完的,而四道台阶全在算法之外;顺带订正正文那句「对拍永远查不出溢出」

原题:洛谷 P1223出自 第 19 章 贪心基础:排序型贪心 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

n 个人在一个水龙头前排队接水,假如每个人接水的时间为 Tᵢ, 请编程找出这 n 个人排队的一种顺序,使得 n 个人的平均等待时间最小。

一个人的等待时间不包括他的接水时间。

如果两个人接水的时间相同,编号更小的人应当排在前面

输入格式

第一行为一个整数 n

第二行 n 个整数,第 i 个整数 Tᵢ 表示第 i 个人的接水时间 Tᵢ

输出格式

输出文件有两行,第一行为一种平均时间最短的排队顺序; 第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。

数据规模与约定

1 ≤ n ≤ 10001 ≤ tᵢ ≤ 10⁶不保证 tᵢ 不重复

输入输出样例

输入

10
56 12 1 99 1000 234 33 55 99 812

输出

3 2 7 8 1 4 9 6 10 5
291.90

第一行是人数,第二行是每个人的接水时间。 ⚠ 这一组样例挡住了本页四个错法中的两个,放过了另外两个 —— 第 ⑤ 步会说这不是运气。

★ 这是第 19 章的本章原题 —— 所以这一页专讲「正文之外」的事

第 19 章的第一道例题就是它,而且正文把算法部分做完了: 按接水时间从小到大排,交换论证两行推导(正文第 ⑤ 步), 再拿 n! 全排列暴力对拍钉死(正文第 ③ 步)。

⇒ 所以这一页不重复那些。它讲的是另一件事:

这道题的算法三分钟就写完了,而它能不能 AC,四道台阶一道都不在算法里。

1算法部分:三分钟

正文已经证过了,这里只把结论抄一遍:按接水时间升序排, 排在第 i+1 位的人,他的接水时间要被后面 n-1-i 个人一起等,于是

    总等待 = Σ (n-1-i) · t[排在第 i+1 位的人]
✓ 顺手再验一次(一条和排序无关的路)

n ≤ 8 时把全部 n! 种排队顺序枚举一遍取最小,和「排序后累加」逐组比:

320 组,不一致 0 组。

⇒ 这一步是白送的:正文那个全排列暴力现成就在 code/19-greedy-sorting/brute.cpp 里。

下面四步全是算法之外的。

2台阶一:要输出的是编号,不是时间

题面要的第一行是「排队顺序」—— 也就是人的编号,不是排好序的接水时间。 所以排的不能是 t 本身,而是 (时间, 编号) 这个 pair

★ 关键的一步

而一旦排的是 pair,题面那句「接水时间相同,编号更小的排在前面」是白拿的:

pair 的默认比较就是「先比 first,再比 second」, 我们把编号放在 second —— 要的 tie-break 一个字都不用写。

注意这个「白拿」是有代价的:它让下一道台阶变得完全看不见。

3★★★ 台阶二:std::sort 不是稳定排序

很多人不写 pair,写一个结构体加一个比较器 —— 而比较器只比时间

p1223Cmp.cpp错法一:比较器只比时间
// P1223 错法一:比较器**只比接水时间** —— 并列时编号顺序不保证
//
// ★★★ 这是这一页的主角,也是这道题最隐蔽的一个 bug:
// 题面白纸黑字写着「**如果两个人接水的时间相同,编号更小的人应当排在前面**」,
// 而 `std::sort` **不是稳定排序** —— 比较器说「这两个不分先后」时,
// 它把谁放前面是没有承诺的。
//
// ⚠ 而它的抓获率有**两个主语**,页面第 ④ 步各量了一条曲线:
// ① **值域**:`t` 的取值范围决定了「有没有并列」。没有并列,这个 bug 根本不存在。
// ② ★★★ **n**:libstdc++ 的 `std::sort` 对**小数组直接走插入排序**(阈值 16),
// 而插入排序恰好是稳定的 ⇒ **n ≤ 16 时这个 bug 是精确的 0**。
// 官方样例 n = 10(而且里面真有一对并列的 99)—— **正好放过**。
//
// ⇒ 这是本书「小数据本身就是覆盖能力」(第 6 章 P8218)那条经验的一个**干净反例**:
// 这个 bug 只在数据**大**的时候才现形。两条经验都对,主语不一样。
//
// 修法有三种,随便挑一种:① 排 `pair` 用默认比较(正解那样);
// ② 比较器写全 `x.t != y.t ? x.t < y.t : x.id < y.id`;③ 换 `stable_sort`。
#include <bits/stdc++.h>
using namespace std;
struct P { int t, id; };
int main() {
int n;
if (!(cin >> n)) return 0;
vector<P> a(n);
for (int i = 0; i < n; i++) { cin >> a[i].t; a[i].id = i + 1; }
sort(a.begin(), a.end(), [](const P& x, const P& y) { return x.t < y.t; }); // ← 只比时间
long long total = 0;
for (int i = 0; i < n; i++) total += (long long)(n - 1 - i) * a[i].t;
for (int i = 0; i < n; i++) printf("%d%c", a[i].id, i + 1 == n ? '\n' : ' ');
printf("%.2f\n", (double)total / n);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

比较器说「这两个不分先后」的时候,std::sort 把谁放前面是没有承诺的 (它不是稳定排序)。而题面偏偏对这件事有要求。

它多久现形一次?两个主语,一条一条拆。

主语一:值域n 固定 1000,只拧 t 的上限)

t 的上限 5 100 10 000 10⁶(顶格)
输入里有并列的轮数 300 300 300 104
只比时间那版的轮数 300 300 300 56
★ 触发条件是两层的,不是一层

顶格那一档,300 轮里 104 轮的输入含并列,可只有 56 轮被抓 —— 有并列只是第一层,并列的那两个还得真的被换过来才现形。

⇒ 这是「触发条件要量不要推」的又一次: 按「有并列就会错」去推,会把抓获率高估将近一倍。

主语二:n(值域固定为 5,保证每一轮都有并列 ⇒ 只剩 n 这一个变量)

n 4 8 16 17 20 32 100 1000
错的轮数(300 轮) 0 0 0 300 300 300 300 300
★★★ 一个精确的台阶:16 是 0,17 是 300

不是「小数据抓获率低」,是精确的 0 直接跳到精确的 300

原因不在算法里,在标准库里:libstdc++ 的 std::sort 对长度 ≤ 16 的数组 直接走插入排序 —— 而插入排序恰好是稳定的

把这条线单独钉一次(全部元素相等,看编号有没有被打乱):

    n <= 16   编号仍然是 1 2 3 ... n     <- 插入排序,稳定
    n == 17   17 个位置全部错位           <- 换成快速排序的那一刻

⇒ ⚠ 而这是实现细节,换个编译器/标准库就可能是别的数。 所以要量,不要背 —— 上面那张表是本机 g++ 跑出来的,不是从书上抄的。

★★★ 这是「小数据本身就是覆盖能力」的一个干净反例

本书第 6 章 P8218 那一页量过一件事,结论是: 对拍的小数据不是「凑合」,小本身就是覆盖能力 (全负矩阵那个 bug:随机 n ≤ 6 抓 23/300,照题面规模 n = 30..1200/300)。

这一页的这个 bug 正好反过来n ≤ 16结构上的 0n ≥ 17 才开始有。

两条都对,主语不一样: P8218 那个 bug 活在「数据小」里,这一个活在「排序算法换挡」里。 ⇒ 所以生成器的规模旋钮两头都要拧,这是本书从第 49 章起反复撞到的同一件事。

4★★★ 台阶三:long long —— 兼给正文的一句话订正

p1223Int.cpp错法二:总等待用 int
// P1223 错法二:总等待时间用 `int`
//
// 顶格 `n = 1000`、`t = 10⁶`:总等待最大 `10⁶ × 1000 × 999 / 2 ≈ 5.0 × 10¹¹`,
// 而 `int` 只到 2 147 483 647 —— 差了两百多倍。
//
// ★ 而这一条正是第 19 章正文第 ⑦ 步那个警告框说的事,那儿写着
// 「**对拍永远不会告诉你这件事** …… 这只能靠脑子」。
// ⚠ **在这道题上那句话不成立**(页面第 ⑤ 步实测):那句话的隐含前提是
// 「参照物是暴力」(第 11 章 P1908 已经把这个主语点出来了)——
// 而这道题的参照物**就是正解本身,只换了一个类型**,顶格随便跑。
//
// ⇒ 顶格档(n = 1000, t ≤ 10⁶)对拍,它**每一轮都被抓**。
// ⚠ 官方样例(n = 10,总等待 2919)当然放过它。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) { cin >> a[i].first; a[i].second = i + 1; }
sort(a.begin(), a.end());
int total = 0; // ← 这里
for (int i = 0; i < n; i++) total += (n - 1 - i) * a[i].first;
for (int i = 0; i < n; i++) printf("%d%c", a[i].second, i + 1 == n ? '\n' : ' ');
printf("%.2f\n", (double)total / n);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

先算,别测。 最坏情况是「所有人的接水时间都顶格」,总等待 = 10⁶ · n(n-1)/2

    n = 66   总等待 2 145 000 000  <  2^31 = 2 147 483 648
    n = 67   总等待 2 211 000 000  >= 2^31        <- 精确分界
    n = 1000(顶格)  499 500 000 000            <- 超了 232 倍

而随机数据的那条线在更后面,差得还不少

n 8 50 65 66 100 1000
int 版错的轮数(300 轮,t ≤ 10⁶ 随机) 0 0 0 0 1 300

随机数据下它第一次出错在 n = 100,要到 n = 130 才 300 轮全错 —— 比算出来的 67 晚了将近一倍。

★★★ 为什么晚:排序这个动作本身,把总等待压小了正好三倍

关键在于大系数配到的是小数值:升序排完之后,系数最大的 n-1 乘的是最小的那个 t

均匀随机的 t 排好序之后 E[s_i] ≈ (i+1)/(n+1) · T,代进去:

    E[总等待] = Σ (n-1-i) · (i+1)/(n+1) · T = T · n(n-1)/6
    全部顶格   =                             T · n(n-1)/2

正好 3 倍。 实测(每档 300 轮):

n 随机数据的平均总等待 全部顶格 比值
100 1 651 417 770 4 950 000 000 3.00
1000 166 360 035 296 499 500 000 000 3.00

⇒ 所以「顶格数据」有两种,而它们差 3 倍:题面规模顶格n = 1000 随机) 和真正的最坏n = 1000 且每个 t 都是 10⁶)。 ★ 这是第 4 章 P1731 那条「数据范围顶格不等于最坏」在算术题上的又一次。

⚠⚠ 而这一档实测,把第 19 章正文里的一句话打回来了

第 19 章正文第 ⑦ 步那个警告框,原话是:

而对拍永远不会告诉你这件事:对拍用的是 n ≤ 8 的小数据, 小数据下 intlong long 的行为完全一样。这只能靠脑子。

那句「永远」是错的。 这一页顶格档(n = 1000t ≤ 10⁶)的对拍, int 版被抓 300 / 300 —— 一轮都没漏。

真正成立的是那句话的前提,而它当时没写出来: 「对拍用的是 n ≤ 8 的小数据」,是因为正文那道题的参照物是 n! 全排列暴力。 而查溢出根本不需要那个参照物 —— 拿同一份算法的 long long当参照物就够了, 这条路没有规模限制,想开多大开多大。

★ 这正是第 11 章 P1908 那条经验的主语: 「对拍查不出溢出」的死结从来不是「溢出」,是那个参照物是暴力。 ⇒ 正文那个框已经按这个改过了(加上了主语)。

5台阶四:输出那两行 —— 以及「样例挡不挡得住」的规律

剩下两个错法都在最后两行输出上,而且都很显眼

p1223Self.cpp错法三:把自己的接水时间也算进等待
// P1223 错法三:把「自己的接水时间」也算进等待
//
// 题面特意写了一句:「**一个人的等待时间不包括他的接水时间**。」
// 这句提醒按第 12 章那套分法是**命门**(不是情报、也不是噪声):
// 照着「等待 = 轮到我之前的全部时间 + 我自己接水」写,系数就从 `n-1-i` 变成 `n-i`,
// 答案每一组都会多出 `sum(t) / n`。
//
// ★ 它错得非常显眼 —— **官方样例当场就挡住了**(532.00 vs 291.90)。
// ⇒ 和上面两个错法凑成这一页的一条观察:
// **官方样例挡住的都是「每一组都错」的,放过的都是「偶尔才错」的。**
// 这不是运气,几乎是必然:样例只有一组,它天生就是一个「一测就死」的过滤器。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) { cin >> a[i].first; a[i].second = i + 1; }
sort(a.begin(), a.end());
long long total = 0;
for (int i = 0; i < n; i++) total += (long long)(n - i) * a[i].first; // ← n-i,多算了自己
for (int i = 0; i < n; i++) printf("%d%c", a[i].second, i + 1 == n ? '\n' : ' ');
printf("%.2f\n", (double)total / n);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面特意写了「一个人的等待时间不包括他的接水时间」—— 按第 12 章那套分法,这句提醒是命门(不是情报,也不是噪声): 系数从 n-1-i 变成 n-i,答案每一组都会多出 sum(t)/n

p1223Div.cpp错法四:平均值用整数除法
// P1223 错法四:平均值用**整数除法**
//
// `total / n` 先把小数截掉了,再交给 `%.2f` —— 打出来永远是 `xxx.00`。
// 官方样例:2919 / 10 = 291 ⇒ `291.00`,而答案是 `291.90`。**样例当场挡住。**
//
// ★ 留着它是为了凑齐这一页那张「样例挡不挡得住」的表:
// 四个错法里,样例挡住的两个都是「每一组都错」的,放过的两个都是「偶尔才错」的。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) { cin >> a[i].first; a[i].second = i + 1; }
sort(a.begin(), a.end());
long long total = 0;
for (int i = 0; i < n; i++) total += (long long)(n - 1 - i) * a[i].first;
for (int i = 0; i < n; i++) printf("%d%c", a[i].second, i + 1 == n ? '\n' : ' ');
printf("%.2f\n", (double)(total / n)); // ← 括号位置:整数除完才转 double
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

(double)(total / n) —— 括号位置错了一格,小数先被截掉,打出来永远是 xxx.00

四个错法在官方样例上的表现:

错法 官方样例 挡住了吗 它多久错一次
比较器只比时间 291.90 放过 顶格 300 轮里 56 轮
总等待用 int 291.90 放过 n ≤ 66 永远不错
多算自己的接水时间 532.00 挡住 每一组都错
平均值整数除法 291.00 挡住 每一组都错(除非答案正好是整数)
★★ 这张表不是运气 —— 官方样例是一个「一测就死」的过滤器

挡住的两个,都是「每一组输入都错」的;放过的两个,都是「偶尔才错」的。

想想就知道这几乎是必然:样例只有一组。 一组数据能挡住的,只有那些「命中率接近 100%」的 bug; 而真正会让你 WA 在第 7 个测试点上的,恰恰是「命中率 20%」的那种 —— 它天然通过样例。

⇒ 所以「样例过了」这句话的信息量,比看上去小得多。 ★ 本书量过好几轮:P1074 的样例 ① 放过一个错法而样例 ② 挡住了、 P2324 挡住了、P1032 两个都没挡住、 P1516 三个全放过。这一页多给了一条能预测的规律。

6正解、度量程序和生成器

p1223.cpp★ 这一版就能 AC
// P1223 排队接水 —— ★ 这一版就能 AC
//
// ★★★ 这道题的算法,第 19 章正文已经**证完了**:
// 按接水时间从小到大排,交换论证两行推导(见 /ch/19-greedy-sorting/ 第 ⑤ 步)。
// ⇒ 写这道题的算法部分只要三分钟:`sort` 一句,累加一句。
//
// 而它能不能 AC,全押在**三件和贪心毫无关系的事**上(页面第 ③ ④ ⑤ 步各量了一件):
// ① 要输出的是**编号**不是时间 —— 所以排的是 `(时间, 编号)` 这个 pair;
// ② 题面那句「**接水时间相同,编号更小的排在前面**」是**命门**:
// 比较器只比时间的话,`std::sort` 不保证并列元素的相对顺序(它不是稳定排序);
// ⚠ 而这个 bug **在 n ≤ 16 时抓不到**(libstdc++ 小数组直接走插入排序),
// 官方样例 n = 10 —— 正好放过;
// ③ 总等待时间要开 **long long**:顶格 `n = 1000`、`t = 10⁶` 时约 5 × 10¹¹。
//
// ★ 第 ② 条这里是**白拿**的:`pair` 的默认比较就是「先比 first,再比 second」,
// 而我们把编号放在 second —— 题面要的 tie-break 一个字都不用写。
//
// 输出:第一行是排队顺序(编号),第二行是平均等待时间,保留两位小数。
// ⚠ 平均要用**浮点**除(`(double)total / n`),整数除法当场就错(样例挡得住这一个)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n); // (接水时间, 编号)
for (int i = 0; i < n; i++) {
cin >> a[i].first;
a[i].second = i + 1;
}
sort(a.begin(), a.end()); // ★ 默认比较 = 先按时间,时间相同按编号
long long total = 0; // ★ 顶格约 5e11,int 装不下
for (int i = 0; i < n; i++)
total += (long long)(n - 1 - i) * a[i].first; // 排第 i+1 位的人,被后面 n-1-i 个人等
for (int i = 0; i < n; i++)
printf("%d%c", a[i].second, i + 1 == n ? '\n' : ' ');
printf("%.2f\n", (double)total / n);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1223Count.cpp度量程序
// P1223 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p1223Count` 人看的版本
// `./p1223Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 六段:
// ① ★ 升序真的最优吗:小 n 全排列穷举,和「排序后累加」逐组比;
// ② ★★ `int` 的**精确分界**:全 t = 10⁶ 时,最小的会溢出的 n(算术题,不用测);
// ③ ★★★ `std::sort` 的**稳定性分界**:全部元素相等时,最小的会打乱编号顺序的 n
// (libstdc++ 小数组走插入排序 —— 这是**实现细节**,所以要量不要背);
// ④ ★★★ 「只比时间」那个 bug 的 **n 曲线**(值域固定为 5,保证有并列 ⇒ 只剩 n 这一个变量);
// ⑤ ★★ 它的**值域曲线**(n 固定 1000)—— 抓获率的第一个主语是「有没有并列」;
// ⑥ ★★ `int` 版的 n 曲线,以及**随机数据**下它开始出错的最小 n
// (⚠ 和第 ② 段那条算出来的线**不是一回事**:一个是最坏,一个是随机)。
#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");
}
struct P { int t, id; };
/** 正解:排 (时间, 编号),返回排好的编号序列 + 总等待 */
static void solve(const vector<int>& t, vector<int>& order, ll& total) {
int n = t.size();
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) a[i] = {t[i], i + 1};
sort(a.begin(), a.end());
order.clear();
total = 0;
for (int i = 0; i < n; i++) {
order.push_back(a[i].second);
total += (ll)(n - 1 - i) * a[i].first;
}
}
/** 错法一:比较器只比时间 */
static void solveCmp(const vector<int>& t, vector<int>& order) {
int n = t.size();
vector<P> a(n);
for (int i = 0; i < n; i++) a[i] = {t[i], i + 1};
sort(a.begin(), a.end(), [](const P& x, const P& y) { return x.t < y.t; });
order.clear();
for (int i = 0; i < n; i++) order.push_back(a[i].id);
}
/** 错法二:总等待用 int 累加(这里真的用 int 加一遍,好把溢出行为原样复现) */
static int solveInt(const vector<int>& t) {
int n = t.size();
vector<int> s = t;
sort(s.begin(), s.end());
int total = 0;
for (int i = 0; i < n; i++) total += (n - 1 - i) * s[i];
return total;
}
static vector<int> gen(mt19937& rng, int n, int hi) {
vector<int> t(n);
for (int i = 0; i < n; i++) t[i] = rng() % (unsigned)hi + 1;
return t;
}
static bool hasDup(vector<int> t) {
sort(t.begin(), t.end());
return adjacent_find(t.begin(), t.end()) != t.end();
}
int main(int argc, char** argv) {
CSV = argc > 1 && string(argv[1]) == "csv";
/* ① 升序真的最优吗 —— 小 n 全排列穷举 */
{
mt19937 rng(20260829u);
int groups = 0, bad = 0;
for (int n = 1; n <= 8; n++) {
for (int rep = 0; rep < 40; rep++, groups++) {
vector<int> t = gen(rng, n, 20);
vector<int> idx(n);
iota(idx.begin(), idx.end(), 0);
ll best = LLONG_MAX;
do {
ll s = 0;
for (int i = 0; i < n; i++) s += (ll)(n - 1 - i) * t[idx[i]];
best = min(best, s);
} while (next_permutation(idx.begin(), idx.end()));
vector<int> ord; ll total;
solve(t, ord, total);
if (total != best) bad++;
}
}
if (!CSV) printf("① 全排列穷举 %d 组(n <= 8):排序贪心和最优解不一致 %d 组\n", groups, bad);
row("opt", {groups, bad});
}
/* ② int 的精确分界:全 t = 10^6 时最小的溢出 n */
{
const ll T = 1000000;
int lim = -1;
for (int n = 1; n <= 1000; n++) {
ll total = T * n * (n - 1) / 2; // 全部相等时的总等待
if (total > 2147483647LL) { lim = n; break; }
}
ll below = T * (ll)(lim - 1) * (lim - 2) / 2, at = T * (ll)lim * (lim - 1) / 2;
ll top = T * 1000LL * 999LL / 2;
if (!CSV) printf("② 全 t = 10^6:n = %d 总等待 %lld(< 2^31),n = %d 是 %lld(>= 2^31);顶格 n = 1000 是 %lld\n",
lim - 1, below, lim, at, top);
row("bound", {lim, below, at, top});
}
/* ③ std::sort 的稳定性分界:全部元素相等,最小的会打乱编号的 n */
{
int lim = -1;
for (int n = 1; n <= 200 && lim < 0; n++) {
vector<int> t(n, 7);
vector<int> ord;
solveCmp(t, ord);
for (int i = 0; i < n; i++) if (ord[i] != i + 1) { lim = n; break; }
}
vector<int> t17(lim, 7), ord17;
solveCmp(t17, ord17);
int moved = 0;
for (int i = 0; i < lim; i++) if (ord17[i] != i + 1) moved++;
if (!CSV) printf("③ 全部相等:n <= %d 时 std::sort 保持编号升序,n = %d 起被打乱(那一档有 %d 个位置错位)\n",
lim - 1, lim, moved);
row("stable", {lim, moved});
}
/* ④ 「只比时间」的 n 曲线(值域固定 5 ⇒ 必有并列) */
{
const int NS[] = {4, 8, 16, 17, 20, 32, 100, 1000};
vector<ll> out;
for (int n : NS) {
mt19937 rng(n * 7919u + 11u);
int bad = 0;
for (int r = 0; r < 300; r++) {
vector<int> t = gen(rng, n, 5);
vector<int> a, b; ll total;
solve(t, a, total);
solveCmp(t, b);
if (a != b) bad++;
}
out.push_back(bad);
if (!CSV) printf("④ n = %4d(t <= 5):只比时间那版错 %d / 300\n", n, bad);
}
row("cmpN", out);
}
/* ⑤ 它的值域曲线(n 固定 1000) */
{
const int HIS[] = {5, 100, 10000, 1000000};
vector<ll> out;
for (int hi : HIS) {
mt19937 rng(hi * 2654435761u + 3u);
int bad = 0, dup = 0;
for (int r = 0; r < 300; r++) {
vector<int> t = gen(rng, 1000, hi);
if (hasDup(t)) dup++;
vector<int> a, b; ll total;
solve(t, a, total);
solveCmp(t, b);
if (a != b) bad++;
}
out.push_back(bad); out.push_back(dup);
if (!CSV) printf("⑤ n = 1000,t <= %7d:只比时间那版错 %d / 300,其中含并列的输入 %d / 300\n", hi, bad, dup);
}
row("cmpHi", out);
}
/* ⑥ int 版的 n 曲线 + 随机数据下的实际分界 */
{
const int NS[] = {8, 50, 65, 66, 100, 1000};
vector<ll> out;
for (int n : NS) {
mt19937 rng(n * 40503u + 7u);
int bad = 0;
for (int r = 0; r < 300; r++) {
vector<int> t = gen(rng, n, 1000000);
vector<int> ord; ll total;
solve(t, ord, total);
if ((ll)solveInt(t) != total) bad++;
}
out.push_back(bad);
if (!CSV) printf("⑥ n = %4d(t <= 10^6):int 版错 %d / 300\n", n, bad);
}
int firstAll = -1, firstAny = -1;
for (int n = 2; n <= 300; n++) {
mt19937 rng(n * 40503u + 7u);
int bad = 0;
for (int r = 0; r < 300; r++) {
vector<int> t = gen(rng, n, 1000000);
vector<int> ord; ll total;
solve(t, ord, total);
if ((ll)solveInt(t) != total) bad++;
}
if (bad > 0 && firstAny < 0) firstAny = n;
if (bad == 300) { firstAll = n; break; }
}
if (!CSV) printf("⑥ 随机 t <= 10^6:int 版第一次出错在 n = %d,到 n = %d 起 300 轮全错\n", firstAny, firstAll);
out.push_back(firstAny); out.push_back(firstAll);
row("intN", out);
}
/* ⑦ ★★★ 为什么随机数据的溢出线比算出来的那条晚一倍:**排序本身把总等待压小了三倍**
升序排好之后,大系数 `n-1-i` 配到的是**小**的那些 t。
随机均匀的 t 排序后 `E[s_i] ≈ (i+1)/(n+1)·T`,代进去:
E[总等待] = Σ (n-1-i)·(i+1)/(n+1)·T = T·n(n-1)/6
而全部 t 都顶格时是 `T·n(n-1)/2` —— **正好 3 倍**。这一段实测那个 3。 */
{
const int NS2[] = {100, 1000};
vector<ll> out;
for (int n : NS2) {
mt19937 rng(n * 99991u + 5u);
long double sum = 0;
for (int r = 0; r < 300; r++) {
vector<int> t = gen(rng, n, 1000000);
vector<int> ord; ll total;
solve(t, ord, total);
sum += (long double)total;
}
ll avg = (ll)(sum / 300 + 0.5L);
ll worst = 1000000LL * n * (n - 1) / 2;
ll ratio100 = (ll)((long double)worst / (long double)avg * 100 + 0.5L);
out.push_back(avg); out.push_back(worst); out.push_back(ratio100);
if (!CSV) printf("⑦ n = %4d:随机数据平均总等待 %lld,全部顶格 %lld,比值 %.2f\n",
n, avg, worst, ratio100 / 100.0);
}
row("ratio", out);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1223Gen.cpp数据生成器
// P1223 对拍生成器:`./p1223Gen <seed> [n] [t 的上限]`
// 默认 `n = 1000`、`t ≤ 10⁶` —— ★ **就是题面的顶格档**。
//
// ★ 默认值写成顶格是有意的(本书第 53 条经验:生成器的默认档位要写成最终档)。
// 这道题的两个隐蔽 bug 恰好各要一头:
// · `int` 溢出要 **n 和 t 都大**(顶格必炸,页面第 ⑤ 步算得出精确分界 n = 66);
// · 排序不稳定要 **n > 16**(小数组走插入排序)**且值域小到有并列**。
// ⇒ 顺手写的 `n ≤ 10`、`t ≤ 100` 那种生成器,**两个都是精确的 0**。
//
// 值域这个旋钮单独拎出来(页面第 ④ 步那张表就是拧它拧出来的):
// `t` 的上限越小,并列越多,「只比时间」那个 bug 越容易现形。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int n = argc > 2 ? atoi(argv[2]) : 1000;
int hi = argc > 3 ? atoi(argv[3]) : 1000000;
n = max(1, min(1000, n));
hi = max(1, min(1000000, hi));
mt19937 rng(seed * 2654435761u + 12345u);
printf("%d\n", n);
for (int i = 0; i < n; i++)
printf("%u%c", (unsigned)(rng() % (unsigned)hi) + 1, i + 1 == n ? '\n' : ' ');
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 生成器的默认档写成了顶格,这次是被逼的

p1223Gen 不带参数就是 n = 1000t ≤ 10⁶ —— 题面顶格。

因为这一页两个隐蔽的 bug 各要一头int 溢出要 nt 都大,排序不稳定要 n > 16 且值域小到有并列。 顺手写的 n ≤ 10t ≤ 100 那种生成器,两个都是精确的 0

7一页纸

关键的一步 没有关键的一步 —— 算法是第 19 章正文证完的,sort 一句
哪一版能 AC p1223.cpp;四个错法都只差一行
台阶一 输出的是编号不是时间 ⇒ 排 (时间, 编号),题面的 tie-break 白拿
台阶二 ★★★ std::sort 不稳定n ≤ 16 精确 0n = 17 精确 300
(libstdc++ 小数组走插入排序 —— 实现细节,要量不要背
台阶三 long long;算出来的分界 n = 67,随机数据要到 n = 130 才全错
排序把总等待压小了正好 3 倍T·n(n-1)/6 vs T·n(n-1)/2
台阶四 等待不含自己的接水时间;平均值要浮点
这一页的主线 有一类题,弯路一步都不在算法里
顺带订正 正文「对拍永远查不出溢出」少了主语 —— 死结是参照物是暴力
而这道题的参照物就是它自己(换个类型),顶格 300 / 300
样例的表现 挡住两个「每组都错」的,放过两个「偶尔才错」的 —— 不是运气