0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1786,日期见页头。两边不一致时信原站。
题目背景
帮派名号:星月家园
帮主尊号:Dragonfly Kang
帮派 ID:2685023
帮派等级:4
帮派人数:101/110
在 absi2011 的帮派里,死号偏多。现在 absi2011 和帮主等人联合决定,要清除一些死号, 加进一些新号,同时还要鼓励帮贡多的人,对帮派进行一番休整。
题目描述
目前帮派内共最多有:
- 1 位帮主(
BangZhu) - 2 位副帮主(
FuBangZhu) - 2 位护法(
HuFa) - 4 位长老(
ZhangLao) - 7 位堂主(
TangZhu) - 25 名精英(
JingYing) - 若干(数量不限)帮众(
BangZhong)
保证以上职位是从高到低排列的。
现在 absi2011 要对帮派内几乎所有人的职位全部调整一番。他发现这是个很难的事情。于是要求你帮他调整。
他给你每个人的以下数据:名字、原来职位、帮贡、等级。
他要按照以下关键字给帮派内的人(帮主、副帮主除外)按以下关键字排序:
- 帮贡(从高到低)第一关键字
- 在输入中出现的顺序(从前到后)第二关键字
然后更新这些人的职位:
- 第 1 ~ 2 名:护法
- 第 3 ~ 6 名:长老
- 第 7 ~ 13 名:堂主
- 第 14 ~ 38 名:精英
- 第 39 ~ (n−3) 名:帮众
可是,乐斗的显示并不按帮贡排序而按职位和等级排序。
他要你按照以下关键字排序并求出最后乐斗显示的列表(在他调整过职位后):
- 职位(从高到低)第一关键字
- 等级(从高到低)第二关键字
- 在输入中出现的顺序(从前到后)第三关键字
注意:absi2011 无权调整帮主、副帮主的职位,包括他自己的。
输入格式
第一行一个正整数 n,表示星月家园内帮友的人数。
下面 n 行每行两个字符串两个整数,表示每个人的名字、职位、帮贡、等级。
输出格式
一共输出 n 行,每行包括排序后乐斗显示的名字、职位、等级。
数据范围
对于 10% 的数据,保证 n = 3。
对于 40% 的数据,保证各个人的帮贡均为 0。
对于 100% 的数据,保证:
3 ≤ n ≤ 1101 ≤ 名字长度 ≤ 30,所有名字两两不同,名字只包含 ASCII 可见字符0 ≤ 各个人的帮贡 ≤ 10⁹1 ≤ 各个人等级 ≤ 150- 职位必定为那七个之一
- 初始时帮派内最多有:1 位帮主、2 位副帮主、2 位护法、4 位长老、7 位堂主、25 名精英
- 恰好有一名帮主,恰好有两名副帮主,且恰好有一名副帮主叫
absi2011
【题目来源】fight.pet.qq.com,absi2011 授权题目。
时限 1 秒,内存 128000 KB(125 MB)。
输入输出样例
输入
9 DrangonflyKang BangZhu 100000 66 RenZaiJiangHu FuBangZhu 80000 60 absi2011 FuBangZhu 90000 60 BingQiLingDeYanLei HuFa 89000 58 Lcey HuFa 30000 49 BangYou3 ZhangLao 1000 1 BangYou1 TangZhu 100 40 BangYou2 JingYing 40000 10 BangYou4 BangZhong 400 1
输出
DrangonflyKang BangZhu 66 RenZaiJiangHu FuBangZhu 60 absi2011 FuBangZhu 60 BingQiLingDeYanLei HuFa 58 BangYou2 HuFa 10 Lcey ZhangLao 49 BangYou1 ZhangLao 40 BangYou3 ZhangLao 1 BangYou4 ZhangLao 1
九个人。要重排的是「帮主、副帮主之外」的那 6 位; 帮贡从高到低是 89000 / 40000 / 30000 / 1000 / 400 / 100 ⇒ 前 2 名当护法、后 4 名当长老。
1★ 先做这一章的那个动作:估一遍 —— 三十秒,而它给出的答案是「怎么写都行」
第 45 章教的动作是「先看数据范围,再决定写多复杂」。这道题的范围是 n ≤ 110。
估:P1177 那一页实测过「O(n²) 排序,n = 2×10⁴ 要 379 毫秒」。
按平方缩回 n = 110:379 ms × (110 / 20000)² ≈ 11.5 微秒。
量:本机顶格 n = 110、两次冒泡、重复 2000 次取平均 ⇒ 10.4 微秒
(A 机 · WSL2 · Linux 6.18-microsoft · nproc 8 · 2026-09-05 · 独占)。
⇒ ★★ 估出来 11.5,量出来 10.4 —— 差 10%。 这一章那把尺子在这道题上是准的。
而时限是 1 秒 = 10⁶ 微秒 ⇒ 余量约 9.5 万倍。
反过来问一遍更直观:按 P1177 那个速率,冒泡一秒大概能排 32487 个数,而这道题只有 110 个 —— 这道题的数据范围离「排序选型开始要紧」那条线,差着 295 倍。
⇒ ★★★ 所以这一章的动作在这里做完之后,结论是一句反过来的话: 这道题的力气一分钱都不该花在算法上。 下面整整一页,讲的全是读题。
- 要不要
long long? 帮贡≤ 10⁹,int上限 2147483647 ⇒ 够,余量 2.15 倍。 (连乘法都没有 —— 这道题从头到尾只比较,不做算术。) - 换个排序算法值多少? 顶格
n = 110:冒泡 11666 次比较、std::sort1744 次 ⇒ 只差 6.7 倍,而两边都是微秒级。
2第 ① 版:照着题面写下来 —— 而第一句规则就漏了
题面把这件事说了两遍:正文里「(帮主、副帮主除外)」,末尾又单独加粗一句 「absi2011 无权调整帮主、副帮主的职位,包括他自己的」。
说两遍的东西,通常是出题人知道大家会漏的东西。 第一版顺手写成「把所有人按帮贡排一排,再按名次发职位」,帮主就被降成护法了。
★ 它的触发条件是这一版自己的排序池,而不是正解的池 ——
⚠ 这一点我一开始算错了:我写的是「排序池非空 ⇒ n ≥ 4」,
可这一版的池是全员,n = 3 时它就有 3 个人。
⇒ 实测第一条线是 n = 3:题面 10% 那一档一分都不给它。
// P1786 —— ✗ 错法一:把**所有人**都丢进帮贡排序里重排职位。//// 题面那句「absi2011 无权调整帮主、副帮主的职位,包括他自己的」是一句**规则**,// 不是背景介绍。漏掉它,帮主会被按帮贡排成护法/长老,副帮主同理。//// ★ 它的触发条件是一句能证的话:**只要排序池非空,帮主的职位就一定被改写** ——// 而排序池非空 ⟺ n ≥ 4。⇒ 题面 10% 那一档(n = 3)是它**能证的精确的 0**。#include <bits/stdc++.h>using namespace std;
/** ★ 七个职位**从高到低**,下标就是它的等级序号 */static const char* POS[7] = { "BangZhu", "FuBangZhu", "HuFa", "ZhangLao", "TangZhu", "JingYing", "BangZhong"};static int posId(const string& s) { for (int i = 0; i < 7; i++) if (s == POS[i]) return i; return -1;}
struct Person { string name; int pos, gong, lv, idx;};
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0; vector<Person> a(n); for (int i = 0; i < n; i++) { string nm, ps; long long g; int lv; cin >> nm >> ps >> g >> lv; a[i] = Person{nm, posId(ps), (int)g, lv, i}; }
// ① 只有帮主(0)、副帮主(1)之外的人参加重排 vector<int> id; for (int i = 0; i < n; i++) id.push_back(i); // ✗ 少了「pos >= 2」这一句
// ② 帮贡从高到低,并列的按输入顺序 sort(id.begin(), id.end(), [&](int x, int y) { if (a[x].gong != a[y].gong) return a[x].gong > a[y].gong; return a[x].idx < a[y].idx; });
// ③ 名次 → 新职位(r 是 0 基的下标,所以「第 1~2 名」就是 r < 2) for (size_t r = 0; r < id.size(); r++) { int p; if (r < 2) p = 2; // 护法 2 人 else if (r < 6) p = 3; // 长老 4 人 else if (r < 13) p = 4; // 堂主 7 人 else if (r < 38) p = 5; // 精英 25 人 else p = 6; // 其余帮众 a[id[r]].pos = p; }
// ④ 输出顺序:职位(高到低)、等级(高到低)、输入顺序 vector<int> ord(n); iota(ord.begin(), ord.end(), 0); sort(ord.begin(), ord.end(), [&](int x, int y) { if (a[x].pos != a[y].pos) return a[x].pos < a[y].pos; if (a[x].lv != a[y].lv) return a[x].lv > a[y].lv; return a[x].idx < a[y].idx; });
// ⑤ 三列:名字、职位、等级 for (int i : ord) cout << a[i].name << ' ' << POS[a[i].pos] << ' ' << a[i].lv << '\n'; return 0;}点「运行 ▶」看结果
3第 ② 版:排除对了,可比较器只抄了题面点名的那两个字
题面两处排序都把「在输入中出现的顺序」写成了最后一个关键字。 很多人会把它读成一句解释(「并列的就按原样放着嘛」),于是比较器里只写帮贡、只写等级。
⚠ std::sort 不保证稳定(第 19 章 P1223 那一跤)。
★★★ 而这个错法的专门档,出题人已经写在题面上了: 「对于 40% 的数据,保证各个人的帮贡均为 0」—— 帮贡全为 0 ⇒ 第一次排序全部并列 ⇒ 名次完全由稳定性说了算。 那一档实测 300 / 300。
把等级压到只有 3 种(逼出并列),只拧 n,每档 300 轮:
n |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 被抓 | 0 | 0 | 0 | 0 | 0 | 0 | ★ 0 | ★ 288 | 290 | 298 | 292 |
★★ 16 和 17 之间是一堵墙,而墙的位置不在题面上 ——
libstdc++ 的 introsort 对长度 ≤ 16 的段直接走插入排序,而插入排序是稳定的。
⇒ 这和 P1223 量到的是同一条线(那道题是 n = 4/8/16 精确的 0、n = 17 起精确的 300)。
⚠ 它是实现细节,所以要量不要背:换一个标准库实现,这堵墙可能在别处,也可能没有。 ⇒ 而对写题的人来说结论只有一句:把「输入顺序」写进比较器,别赌那 16。
4第 ③ 版:职位拿字符串比大小 —— 而这条线在 n = 10
抄那张七个职位的表很烦,顺手就会写 a.pos < b.pos(字符串比较)。可两个顺序不是一回事:
字典序升序:BangZhong < BangZhu < FuBangZhu < HuFa < JingYing < TangZhu < ZhangLao
真正的高低:BangZhu > FuBangZhu > HuFa > ZhangLao > TangZhu > JingYing > BangZhong★ 巧的是前三个完全一致(BangZhu / FuBangZhu / HuFa),帮众又要到第 39 名才出现。
⇒ 要「长老」和「堂主」同时出场才分得出来 ⇒ 排序池得有 7 个人 ⇒ n ≥ 10。
★★ 实测第一条线正是 n = 10 —— 而官方样例的 n 是 9。
5★ 正解 —— 就是第一版写对的那一版,它已经能过了
| 规则 | 一行 |
|---|---|
| 谁参加重排 | if (a[i].pos >= 2) id.push_back(i); |
| 重排的关键字 | 帮贡↓,并列按输入序 |
| 名次 → 职位 | r < 2 / 6 / 13 / 38(★ r 是 0 基的,所以是 < 不是 <=) |
| 输出的关键字 | 职位↓(查那张七级表)、等级↓、输入序↑ |
| 输出哪三列 | 名字、职位、等级 —— 帮贡不打 |
★ 两次排序都把「输入顺序」写成最后一个关键字之后,比较器成了严格全序
⇒ std::sort 稳不稳定就都无所谓了。这是绕开上面那堵 16 的墙的正经办法。
// P1786 帮贡排序 —— 正解。//// ★ 这道题的复杂度估算三十秒就做完了:n ≤ 110,两次排序怎么写都够(见解析页第 ① 步)。// 所以这一份唯一要小心的是**读题**:五条规则,一条都不能读漏。//// ① 要重排的只有「帮主、副帮主之外」的那 n−3 个人;// ② 重排的关键字是 帮贡↓,并列时按**输入顺序**;// ③ 名次 → 新职位:1~2 护法、3~6 长老、7~13 堂主、14~38 精英、39~ 帮众;// ④ 最后输出按 职位↓(七级的高低,**不是字典序**)、等级↓、输入顺序;// ⑤ 输出三列:名字、职位、**等级** —— 帮贡不打。//// ⚠ 两次排序都把「输入顺序」写成**最后一个关键字**,于是 std::sort 稳不稳定都无所谓// (比较器成了严格全序)。[第 19 章 P1223](/sol/p1223/) 那一跤的正面写法。#include <bits/stdc++.h>using namespace std;
/** ★ 七个职位**从高到低**,下标就是它的等级序号 */static const char* POS[7] = { "BangZhu", "FuBangZhu", "HuFa", "ZhangLao", "TangZhu", "JingYing", "BangZhong"};static int posId(const string& s) { for (int i = 0; i < 7; i++) if (s == POS[i]) return i; return -1;}
struct Person { string name; int pos, gong, lv, idx;};
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0; vector<Person> a(n); for (int i = 0; i < n; i++) { string nm, ps; long long g; int lv; cin >> nm >> ps >> g >> lv; a[i] = Person{nm, posId(ps), (int)g, lv, i}; }
// ① 只有帮主(0)、副帮主(1)之外的人参加重排 vector<int> id; for (int i = 0; i < n; i++) if (a[i].pos >= 2) id.push_back(i);
// ② 帮贡从高到低,并列的按输入顺序 sort(id.begin(), id.end(), [&](int x, int y) { if (a[x].gong != a[y].gong) return a[x].gong > a[y].gong; return a[x].idx < a[y].idx; });
// ③ 名次 → 新职位(r 是 0 基的下标,所以「第 1~2 名」就是 r < 2) for (size_t r = 0; r < id.size(); r++) { int p; if (r < 2) p = 2; // 护法 2 人 else if (r < 6) p = 3; // 长老 4 人 else if (r < 13) p = 4; // 堂主 7 人 else if (r < 38) p = 5; // 精英 25 人 else p = 6; // 其余帮众 a[id[r]].pos = p; }
// ④ 输出顺序:职位(高到低)、等级(高到低)、输入顺序 vector<int> ord(n); iota(ord.begin(), ord.end(), 0); sort(ord.begin(), ord.end(), [&](int x, int y) { if (a[x].pos != a[y].pos) return a[x].pos < a[y].pos; if (a[x].lv != a[y].lv) return a[x].lv > a[y].lv; return a[x].idx < a[y].idx; });
// ⑤ 三列:名字、职位、等级 for (int i : ord) cout << a[i].name << ' ' << POS[a[i].pos] << ' ' << a[i].lv << '\n'; return 0;}点「运行 ▶」看结果
下面这一份的比较器和错法二一模一样 —— 两个都没写「输入顺序」。
可它把 std::sort 换成了手写冒泡,于是:
| 比较器 | 排序 | 结果 | |
|---|---|---|---|
| ✗ 错法二 | 没写输入序 | std::sort(不稳定) |
WA(40% 那一档 300 / 300 被抓) |
| ★ 冒泡版 | 没写输入序 | 冒泡(稳定) | ★ AC(1200 轮和正解逐字节相同) |
⇒ ★★★ 复杂度更差的那个写法,在这道题上反而少一个坑。 而你之所以敢用它,靠的正是第 ① 步那三十秒:11666 次比较,余量九万倍。
⇒ 这就是第 45 章那句「先看范围再决定写多复杂」的另一半: 估算不只回答「够不够快」,它还回答「可以放心写笨的」—— 而这道题里,写笨的那一版恰好绕开了整页最难发现的那个 bug。
6⚠ 官方样例的 n 是 9 —— 而这一页五条线是 3 / 3 / 6 / 10 / 17
| 版本 | 第一条线 | 样例 n = 9 |
为什么 |
|---|---|---|---|
| ✗ 全员重排 | n = 3 |
★ 一测就死 | 帮主当场被降成护法 |
| ✗ 等级方向反了 | n = 3 |
★ 一测就死 | 四位长老的等级 49 / 40 / 1 / 1 |
| ✗ 分段 off-by-one | n = 6 |
★ 一测就死 | 打出 3 个护法 |
| ✗ 职位比字典序 | n = 10 |
⚠ 放过 | ★ 样例只到 9 —— 差一个人 |
| ✗ 漏掉输入顺序 | n = 17 |
⚠ 放过 | ★ 9 ≤ 16 ⇒ std::sort 走插入排序,稳定 |
⇒ ★★★ 两个被放过的错法,原因是同一件事:
样例的 n 恰好卡在那两条线的下面。
这不是「样例太小所以测不出」这种模糊说法 —— 它是两个精确的整数:
再多一个人(n = 10)就能挡住字典序,再多八个(n = 17)就能挡住不稳定。
⇒ 「这组样例在结构上问不出这个问题」的又一次, ★ 而这一次「结构」就是一个数字。
7★ 对拍:这道题只有一种算法,所以真正的参照物是「把题面抄成断言」
这道题没有第二种算法(第 12 章 P1010 那种处境), 所以拿另一份「排序 + 切段」当参照物,等于把同一个笔误犯两遍。
出路是「验算走一条和算法完全无关的路」:把题面那几句话逐条抄成断言。 验证器只做六件事,一次排序都没有:
- 输出恰好
n行,名字集合 ≡ 输入的名字集合; - 每人的等级 ≡ 输入里的等级(那一列不许被改);
- 帮主、副帮主的职位 ≡ 输入(「无权调整」那一句);
- 七个职位的人数分别是 1 / 2 / min(2,m) / min(4,·) / min(7,·) / min(25,·) / 其余(
m = n − 3); - ★ 任取两个被重排的人:帮贡更高(并列时输入更靠前)的那个,新职位不能更低 —— 两两比较,不算名次;
- 输出的行序按(职位↓、等级↓、输入序↑)严格递增。
★★★ 而它和逐字节对拍的判决,1200 轮 × 6 个版本、24 格一个不差。 这不是巧合,它是能证的:上面这六条唯一确定了那份输出 —— ⇒ 换句话说,这个「一个不差」本身就是一句结论:题面写下的规则确实定死了答案。
| 档位(每档 300 轮) | ★ 冒泡版 | ✗ 全员重排 | ✗ 漏输入序 | ✗ 字典序 | ✗ 等级反 | ✗ off-by-one |
|---|---|---|---|---|---|---|
0 ★ 顺手写的(n ∈ [3, 20]) |
0 | 300 | ★ 4 | 186 | 300 | 244 |
1 ⚠ 题面 10% 档:n = 3 |
0 | 300 | ★ 0 | ★ 0 | 299 | ★ 0 |
| 2 ⚠ 题面 40% 档:帮贡全为 0 | 0 | 300 | ★ 300 | 300 | 300 | 300 |
3 ★ 顶格(n = 110、等级只有 10 种) |
0 | 300 | 300 | 300 | 300 | 300 |
★★★ 四条读得出来的结论:
-
★★★ 同一个档位,一半打死一半打不着 —— 题面 10% 那一档(
n = 3)里, 正解的排序池一个人都没有 ⇒「漏输入序」「字典序」「off-by-one」三个是能证的精确的 0; ⚠ 而「全员重排」照样 300(它的池是全员)、「等级反」照样 299(还剩两位副帮主要比等级)。 ⇒ ★★ 「这个档位能不能抓到」要按那一版自己在做什么算,不能拿正解的量去推 —— 这正是我在第 ② 步算错的那件事。 -
★★ 题面那两个部分分档,一个是盲区、一个是专门档 —— 10% 档(
n = 3)替三个错法白送 10 分; 而 40% 档(帮贡全为 0)让第一次排序全部并列,把「漏输入序」从 4 顶到 300。 ⇒ 「题面上那几行数字,每一行都是一件工具」的又一次, ★ 而这次同一张表里两行的方向正好相反。 -
★★★ 三个「触发 ≡ 抓获」一个不差,而第四个差 20 倍(都在档 0,
n随机 3~20):版本 第一层:满足触发条件的轮数 真被抓 比 ✗ off-by-one n ≥ 6:244244 ★ 1.0 ✗ 字典序 n ≥ 10:186186 ★ 1.0 ✗ 等级反(档 1) 两位副帮主等级不同:299 299 ★ 1.0 ✗ 漏输入序 n ≥ 17:81★ 4 ⚠ 20.3 倍 ⇒ ★★ 前三条能写成
≡,是因为它们的触发条件只有一层,而且是一个整数; 最后那条有两层(n ≥ 17且 那一组里真有等级并列 且sort真把它换了过来)。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次。 -
★ 冒泡版四档 1200 轮和正解逐字节相同,0 次不一致 —— 上面第 ⑤ 步那句话是量过的。
8★ 哪一版就已经能过了
// P1786 解析页上所有数字的出处。./p1786Count [csv]//// 四件事:// ① ★★★ **三条「从哪个 n 开始才可能错」的线** —— 它们是三个精确的整数,// 而且都能直接从题面那五行名次算出来(4 / 6 / 10),不用跑程序也知道;// 这里把 n = 3..110 每档随机 300 组全跑一遍,看实测对不对得上。// ② 这道题的排序要做多少次比较:冒泡 vs std::sort(n = 110 顶格);// ③ [第 45 章](/ch/45-estimate/)那把尺子:拿 [P1177](/sol/p1177/) 那页实测的// 「`O(n²)` 排序 n = 2×10⁴ 要 379 毫秒」按平方缩回 n = 110,看估得准不准;// ④ 几笔一眼就能算完的账:`int` 够不够、排序池在 n = 3 时有几个人。#include <bits/stdc++.h>using namespace std;
static const char* POS[7] = { "BangZhu", "FuBangZhu", "HuFa", "ZhangLao", "TangZhu", "JingYing", "BangZhong"};static const int CAP[7] = {1, 2, 2, 4, 7, 25, 1000};
struct Person { string name; int pos, gong, lv, idx;};
/** 造一组合法输入(n 固定):恰好 1 帮主 / 2 副帮主,初始职位不超上限,顺序打乱 */static vector<Person> genN(mt19937& rng, int n, bool zeroGong) { vector<int> pos{0, 1, 1}; for (int p = 2; p <= 6 && (int)pos.size() < n; p++) { int room = min(CAP[p], n - (int)pos.size()); int take = (p == 6) ? room : (int)(rng() % (unsigned)(room + 1)); for (int i = 0; i < take; i++) pos.push_back(p); } while ((int)pos.size() < n) pos.push_back(6); vector<Person> a(n); for (int i = 0; i < n; i++) { unsigned rg = zeroGong ? 0u : rng() % 1000000001u; unsigned rv = rng() % 150u; a[i] = Person{"n" + to_string(i), pos[i], (int)rg, 1 + (int)rv, 0}; } shuffle(a.begin(), a.end(), rng); for (int i = 0; i < n; i++) a[i].idx = i; return a;}
/** 六个版本共用的骨架。which: 0 正解 / 1 全员重排 / 2 不稳定 / 3 字典序 / 4 等级升序 / 5 分段 off-by-one */static string solve(vector<Person> a, int which) { int n = (int)a.size(); vector<int> id; for (int i = 0; i < n; i++) if (which == 1 || a[i].pos >= 2) id.push_back(i);
if (which == 2) { // ✗ 比较器里没有「输入顺序」,而 std::sort 不稳定 sort(id.begin(), id.end(), [&](int x, int y) { return a[x].gong > a[y].gong; }); } else { sort(id.begin(), id.end(), [&](int x, int y) { if (a[x].gong != a[y].gong) return a[x].gong > a[y].gong; return a[x].idx < a[y].idx; }); }
for (size_t r = 0; r < id.size(); r++) { int p; if (which == 5) { // ✗ 照题面「第 1~2 名」字面抄,而 r 是 0 基的 if (r <= 2) p = 2; else if (r <= 6) p = 3; else if (r <= 13) p = 4; else if (r <= 38) p = 5; else p = 6; } else { if (r < 2) p = 2; else if (r < 6) p = 3; else if (r < 13) p = 4; else if (r < 38) p = 5; else p = 6; } a[id[r]].pos = p; }
vector<int> ord(n); iota(ord.begin(), ord.end(), 0); auto cmp = [&](int x, int y) { if (a[x].pos != a[y].pos) { if (which == 3) return string(POS[a[x].pos]) < string(POS[a[y].pos]); // ✗ 字典序 return a[x].pos < a[y].pos; } if (a[x].lv != a[y].lv) return which == 4 ? a[x].lv < a[y].lv : a[x].lv > a[y].lv; return a[x].idx < a[y].idx; }; if (which == 2) { sort(ord.begin(), ord.end(), [&](int x, int y) { if (a[x].pos != a[y].pos) return a[x].pos < a[y].pos; return a[x].lv > a[y].lv; }); } else { sort(ord.begin(), ord.end(), cmp); }
string s; for (int i : ord) { s += a[i].name; s += ' '; s += POS[a[i].pos]; s += ' '; s += to_string(a[i].lv); s += '\n'; } return s;}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
// ① 三条线:从哪个 n 开始,随机数据里第一次出现和正解不同 const int TRIALS = 300; int firstN[6] = {0, 0, 0, 0, 0, 0}; for (int n = 3; n <= 110; n++) { mt19937 rng(20260905u + (unsigned)n * 7919u); for (int t = 0; t < TRIALS; t++) { vector<Person> a = genN(rng, n, false); string ref = solve(a, 0); for (int w = 1; w <= 5; w++) if (!firstN[w] && solve(a, w) != ref) firstN[w] = n; } }
// ②′ 那条阈值线:等级只有 3 种(大量并列),只拧 n string tieScan; int tieFirst = 0; for (int n = 10; n <= 24; n++) { mt19937 rng(20260905u + (unsigned)n * 104729u); int bad = 0; for (int t = 0; t < 300; t++) { vector<Person> a = genN(rng, n, false); for (auto& p : a) p.lv = 1 + (int)(rng() % 3u); if (solve(a, 2) != solve(a, 0)) bad++; } if (bad && !tieFirst) tieFirst = n; tieScan += to_string(n) + ":" + to_string(bad) + (n == 24 ? "" : " "); }
// ② 比较次数(顶格 n = 110):冒泡是死的公式,std::sort 拿计数比较器数 const int N = 110; long long cmpBubble = (long long)(N - 3) * (N - 4) / 2 + (long long)N * (N - 1) / 2; long long cmpSort = 0; { mt19937 rng(20260905u); vector<Person> a = genN(rng, N, false); vector<int> id; for (int i = 0; i < N; i++) if (a[i].pos >= 2) id.push_back(i); sort(id.begin(), id.end(), [&](int x, int y) { cmpSort++; if (a[x].gong != a[y].gong) return a[x].gong > a[y].gong; return a[x].idx < a[y].idx; }); vector<int> ord(N); iota(ord.begin(), ord.end(), 0); sort(ord.begin(), ord.end(), [&](int x, int y) { cmpSort++; if (a[x].pos != a[y].pos) return a[x].pos < a[y].pos; if (a[x].lv != a[y].lv) return a[x].lv > a[y].lv; return a[x].idx < a[y].idx; }); }
// ③ 顶格冒泡真跑多久:★ 亚毫秒的量必须重复很多次取平均([第 38 章那一跤](/sol/p1177/)) const int REP = 2000; double usBubble = 0; { mt19937 rng(20260905u); vector<Person> base = genN(rng, N, false); auto t0 = chrono::steady_clock::now(); long long sink = 0; for (int r = 0; r < REP; r++) { vector<Person> a = base; vector<int> id; for (int i = 0; i < N; i++) if (a[i].pos >= 2) id.push_back(i); for (size_t i = 0; i + 1 < id.size(); i++) for (size_t j = 0; j + 1 + i < id.size(); j++) if (a[id[j]].gong < a[id[j + 1]].gong) swap(id[j], id[j + 1]); vector<int> ord(N); iota(ord.begin(), ord.end(), 0); for (int i = 0; i + 1 < N; i++) for (int j = 0; j + 1 + i < N; j++) { const Person& x = a[ord[j]]; const Person& y = a[ord[j + 1]]; bool worse = (x.pos != y.pos) ? (x.pos > y.pos) : (x.lv < y.lv); if (worse) swap(ord[j], ord[j + 1]); } sink += ord[0] + id[0]; } auto t1 = chrono::steady_clock::now(); usBubble = chrono::duration<double, micro>(t1 - t0).count() / REP; if (sink == -1) puts(""); // 防止整段被优化掉 } // 拿 P1177 那页的实测按平方缩回来:379 毫秒 @ n = 2×10⁴ double extrapUs = 379000.0 * (110.0 / 20000.0) * (110.0 / 20000.0);
// ④ 几笔算得完的账 long long intMargin = 2147483647LL / 1000000000LL; // 帮贡 ≤ 10⁹,int 上限的几倍 int poolAt3 = 0; // n = 3 时排序池有几个人 long long oneSecN = (long long)(20000.0 * sqrt(1000.0 / 379.0)); // 冒泡一秒大概能排多少个数
if (csv) { printf("firstAll,%d\n", firstN[1]); printf("firstUnstable,%d\n", firstN[2]); printf("firstStr,%d\n", firstN[3]); printf("firstAsc,%d\n", firstN[4]); printf("firstOff,%d\n", firstN[5]); printf("tieFirst,%d\n", tieFirst); printf("tieScan,%s\n", tieScan.c_str()); printf("cmpBubble,%lld\n", cmpBubble); printf("cmpSort,%lld\n", cmpSort); printf("usBubble,%.1f\n", usBubble); printf("extrapUs,%.1f\n", extrapUs); printf("usInBand,%d\n", (usBubble > 1.0 && usBubble < 500.0) ? 1 : 0); printf("marginK,%d\n", (int)(1000000.0 / usBubble / 1000.0)); printf("intMargin,%lld\n", intMargin); printf("poolAt3,%d\n", poolAt3); printf("oneSecN,%lld\n", oneSecN); return 0; }
puts("① ★★★ 三条线:从哪个 n 开始,这个版本才**可能**和正解不同(n = 3..110,每档 300 组随机)"); puts(""); puts("| 版本 | 实测第一个 n | 不跑程序也算得出来的理由 |"); puts("|---|---|---|"); printf("| ✗ 全员重排 | **%d** | ⚠ 我先算成 4(n−3 ≥ 1),错了:**它的池是全员**,n = 3 时就有 3 个人 |\n", firstN[1]); printf("| ✗ 分段 off-by-one | **%d** | 要有第 3 名才看得出「护法多了一个」⇒ n − 3 ≥ 3 |\n", firstN[5]); printf("| ✗ 职位比字典序 | **%d** | 字典序和高低序前三个一致,要长老+堂主同时出现 ⇒ n − 3 ≥ 7 |\n", firstN[3]); printf("| ✗ 等级方向反了 | **%d** | 两位副帮主就能比等级 ⇒ n = 3 也活着 |\n", firstN[4]); printf("| ✗ 漏掉输入顺序 | **%d** | ★★★ 正好是 libstdc++ 那条**插入排序阈值 16** 加一(见 ②′)|\n", firstN[2]); puts(""); printf("⇒ ★★ 题面「10%% 的数据 n = 3」那一档,**正解的**排序池有 **%d** 个人 ——\n", poolAt3); puts(" 于是「分段 off-by-one」和「职位比字典序」在那一档是能证的 0,"); puts(" ⚠ 而「全员重排」和「等级方向反了」照样活着 —— **同一个档位,一半打死一半打不着**。"); puts(""); puts("②′ ★★★ 那条 n = 17 的线是从哪儿来的:把等级压到 3 种(逼出并列),只拧 n"); puts(""); printf(" %s\n", tieScan.c_str()); printf(" ⇒ 第一个非零的 n 是 **%d** —— 而 libstdc++ 的 introsort 对长度 ≤ **16** 的段\n", tieFirst); puts(" 直接走插入排序,插入排序是稳定的。⚠ 这是**实现细节**,所以要量不要背"); puts(" ([第 19 章 P1223](/sol/p1223/) 量到的是同一条线)。"); puts(""); puts("② 这道题的排序要做多少次比较(顶格 n = 110)"); printf(" 冒泡(两次): %lld 次\n", cmpBubble); printf(" std::sort : %lld 次 ⇒ 只差 %.1f 倍\n", cmpSort, (double)cmpBubble / (double)cmpSort); puts(""); puts("③ ★ 第 45 章那把尺子:估一遍,再量一遍"); printf(" 估:P1177 那页实测「O(n²) 排序 n = 2×10⁴ → 379 毫秒」,按平方缩回 n = 110 ⇒ 约 %.1f 微秒\n", extrapUs); printf(" 量:本机顶格冒泡(重复 %d 次取平均)⇒ %.1f 微秒\n", REP, usBubble); printf(" ⇒ 时限 1 秒 = 10⁶ 微秒,余量约 %d 千倍 —— 怎么写都行\n", (int)(1000000.0 / usBubble / 1000.0)); printf(" ⇒ 反过来问:冒泡一秒大概能排 %lld 个数,而这道题只有 110 个\n", oneSecN); puts(""); puts("④ 几笔算得完的账"); printf(" 帮贡 ≤ 10⁹,int 上限 2147483647 ⇒ 够,余量 %.2f 倍(不用 long long)\n", 2147483647.0 / 1e9); return 0;}点「运行 ▶」看结果
| 写法 | 顶格 n = 110 的比较次数 |
交上去 |
|---|---|---|
★ 正解(两次 std::sort) |
1744 | ★ AC |
| ★ 冒泡版(两次冒泡) | 11666(多 6.7 倍,仍是 10.4 微秒) | ★ AC |
| ✗ 漏掉输入顺序 | 一样 | ✗ WA(40% 那一档全错) |
| ✗ 职位比字典序 | 一样 | ✗ WA(n ≥ 10 起) |
| ✗ 分段 off-by-one | 一样 | ✗ WA(n ≥ 6 起) |
| ✗ 全员重排 | 一样 | ✗ WA(n ≥ 3 起,一分不给) |
⇒ ★★★ 这一页从头到尾只有一个秒数(10.4 微秒),而且它是用来说明「这个数不重要」的。 这道题的五个关卡全在读题上: 谁参加重排 / 并列时按输入顺序 / 名次是 1 基而下标是 0 基 / 职位的高低不是字典序 / 输出哪三列。
⇒ ★★ 而第 45 章那把尺子在这里的作用,恰恰是把注意力从算法上挪开: 三十秒算完「余量九万倍」,剩下的时间才够你把题面读第三遍。