题单 · 习题解析

洛谷 P1786 帮贡排序

★★★ 这一章那把尺子在这道题上量出的不是「够不够快」,而是「**别优化**」——「`O(n²)` 排序 n = 2×10⁴ 要 379 毫秒」([P1177](/sol/p1177/) 那页的实测)按平方缩回 `n = 110` 是 **11.5 微秒**,实测 **10.4** 微秒,**估得差 10%**,离时限余量 **9.5 万倍** ⇒ 三十秒算完就该把力气全挪到读题上;★★★ 而这一页最值钱的一条是它的反面:**把 `std::sort` 换成冒泡,同一个「漏掉输入顺序关键字」的疏忽从 WA 变成 AC** —— 冒泡是稳定的 ⇒ **复杂度更差的写法反而少一个坑**;★★★ 五个错法「从哪个 n 开始才可能错」是五个**精确的整数**(3 / 3 / 6 / 10 / **17**),而 **17 恰好是 libstdc++ 那条插入排序阈值 16 加一**([P1223](/sol/p1223/) 量到的同一堵墙,扫描表 16 → 0、17 → 288)—— ⚠ **官方样例的 n 是 9,正好跨过前三条、卡在后两条下面**,两个被放过的错法原因是同一个数字;★★ 题面那两个部分分档**方向正好相反**:10% 档(n = 3)是三个错法白拿 10 分的**盲区**,40% 档(帮贡全为 0)让第一次排序**全部并列**、把「漏输入序」从 4 顶到 **300**;★★ 这道题没有第二种算法 ⇒ 参照物只能是**把题面六条规则抄成断言的验证器**(一次排序都不排、两两比较代替算名次),而它和逐字节对拍**1200 轮 × 6 个版本、24 格一个不差** —— ★ 这个「一个不差」本身就是结论:那六条规则唯一确定了答案;⚠ 外加一条自己踩的:**触发条件要按「那一版自己在做什么」算** —— 我把「全员重排」的线算成 4(用了正解的池 n−3),真值是 **3**

原题:洛谷 P1786出自 第 45 章 复杂度估算与考场策略 的题单题面本地存档:2026-09-05
⚠ 先自己写一遍,再往下看

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

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 ≤ 110
  • 1 ≤ 名字长度 ≤ 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 = 110379 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::sort 1744 次 ⇒ 只差 6.7 倍,而两边都是微秒级。

2第 ① 版:照着题面写下来 —— 而第一句规则就漏了

⚠ 「帮主、副帮主除外」是一条规则,不是背景介绍

题面把这件事说了两遍:正文里「(帮主、副帮主除外)」,末尾又单独加粗一句 「absi2011 无权调整帮主、副帮主的职位,包括他自己的」。

说两遍的东西,通常是出题人知道大家会漏的东西。 第一版顺手写成「把所有人按帮贡排一排,再按名次发职位」,帮主就被降成护法了。

★ 它的触发条件是这一版自己的排序池,而不是正解的池 —— ⚠ 这一点我一开始算错了:我写的是「排序池非空 ⇒ n ≥ 4」, 可这一版的池是全员n = 3 时它就有 3 个人。 ⇒ 实测第一条线是 n = 3:题面 10% 那一档一分都不给它

p1786All.cpp✗ 错法一:全员一起重排 —— 帮主被降职了
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3第 ② 版:排除对了,可比较器只抄了题面点名的那两个字

★★★ 「在输入中出现的顺序」是关键字,不是注解 —— 而 std::sort 不稳定

题面两处排序都把「在输入中出现的顺序」写成了最后一个关键字。 很多人会把它读成一句解释(「并列的就按原样放着嘛」),于是比较器里只写帮贡、只写等级。

std::sort 不保证稳定第 19 章 P1223 那一跤)。

★★★ 而这个错法的专门档,出题人已经写在题面上了: 「对于 40% 的数据,保证各个人的帮贡均为 0」—— 帮贡全为 0 ⇒ 第一次排序全部并列 ⇒ 名次完全由稳定性说了算。 那一档实测 300 / 300

p1786Unstable.cpp✗ 错法二:比较器漏掉「输入顺序」+ std::sort
★★★ 而它第一次出错的 n 是一个精确的整数:17

把等级压到只有 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。

p1786Str.cpp✗ 错法三:职位按字符串字典序排
p1786Off.cpp✗ 错法四:把「第 1~2 名」照字面写成 r ≤ 2(r 是 0 基的)
p1786Asc.cpp✗ 错法五:等级写成了从低到高

5★ 正解 —— 就是第一版写对的那一版,它已经能过了

★ 五条规则,一条不漏就完了;而每一条都只有一行
规则 一行
谁参加重排 if (a[i].pos >= 2) id.push_back(i);
重排的关键字 帮贡↓,并列按输入序
名次 → 职位 r < 2 / 6 / 13 / 38(★ r 是 0 基的,所以是 < 不是 <=
输出的关键字 职位↓(查那张七级表)、等级↓、输入序↑
输出哪三列 名字、职位、等级 —— 帮贡不打

★ 两次排序都把「输入顺序」写成最后一个关键字之后,比较器成了严格全序std::sort 稳不稳定就都无所谓了。这是绕开上面那堵 16 的墙的正经办法。

p1786.cpp★ 正解:两次排序,AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 而这道题最值钱的一句在这儿:把排序换成冒泡,它不但照样 AC,还免费少一个坑

下面这一份的比较器和错法二一模一样 —— 两个都没写「输入顺序」。 可它把 std::sort 换成了手写冒泡,于是:

比较器 排序 结果
✗ 错法二 没写输入序 std::sort(不稳定) WA(40% 那一档 300 / 300 被抓)
★ 冒泡版 没写输入序 冒泡(稳定 AC(1200 轮和正解逐字节相同)

⇒ ★★★ 复杂度更差的那个写法,在这道题上反而少一个坑。 而你之所以敢用它,靠的正是第 ① 步那三十秒:11666 次比较,余量九万倍。

⇒ 这就是第 45 章那句「先看范围再决定写多复杂」的另一半: 估算不只回答「够不够快」,它还回答「可以放心写笨的」—— 而这道题里,写笨的那一版恰好绕开了整页最难发现的那个 bug。

p1786Bubble.cpp★ 冒泡版:同样 AC —— 而且它天然稳定

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 ≤ 16std::sort 走插入排序,稳定

⇒ ★★★ 两个被放过的错法,原因是同一件事样例的 n 恰好卡在那两条线的下面。 这不是「样例太小所以测不出」这种模糊说法 —— 它是两个精确的整数: 再多一个人(n = 10)就能挡住字典序,再多八个(n = 17)就能挡住不稳定。

「这组样例在结构上问不出这个问题」的又一次, ★ 而这一次「结构」就是一个数字。

7★ 对拍:这道题只有一种算法,所以真正的参照物是「把题面抄成断言」

★★★ 验证器一次序都不排、一个名次都不算 —— 它和正解一行代码都不共享

这道题没有第二种算法(第 12 章 P1010 那种处境), 所以拿另一份「排序 + 切段」当参照物,等于把同一个笔误犯两遍。

出路是「验算走一条和算法完全无关的路」把题面那几句话逐条抄成断言。 验证器只做六件事,一次排序都没有

  1. 输出恰好 n 行,名字集合 ≡ 输入的名字集合;
  2. 每人的等级 ≡ 输入里的等级(那一列不许被改);
  3. 帮主、副帮主的职位 ≡ 输入(「无权调整」那一句);
  4. 七个职位的人数分别是 1 / 2 / min(2,m) / min(4,·) / min(7,·) / min(25,·) / 其余(m = n − 3);
  5. 任取两个被重排的人:帮贡更高(并列时输入更靠前)的那个,新职位不能更低 —— 两两比较,不算名次
  6. 输出的行序按(职位↓、等级↓、输入序↑)严格递增。

★★★ 而它和逐字节对拍的判决,1200 轮 × 6 个版本、24 格一个不差。 这不是巧合,它是能证的:上面这六条唯一确定了那份输出 —— ⇒ 换句话说,这个「一个不差」本身就是一句结论:题面写下的规则确实定死了答案。

p1786Check.cpp★ 验证器:把题面六条规则抄成断言,一次排序都没有
p1786Gen.cpp(四档)生成器:顺手写的 / 题面 10% 档 / 题面 40% 档 / 顶格
★★ 四档 × 六个版本 —— ⚠ 而题面那两个部分分档,一个是盲区、一个是专门档
档位(每档 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

★★★ 四条读得出来的结论:

  1. ★★★ 同一个档位,一半打死一半打不着 —— 题面 10% 那一档(n = 3)里, 正解的排序池一个人都没有 ⇒「漏输入序」「字典序」「off-by-one」三个是能证的精确的 0; ⚠ 而「全员重排」照样 300(它的池是全员)、「等级反」照样 299(还剩两位副帮主要比等级)。 ⇒ ★★ 「这个档位能不能抓到」要按那一版自己在做什么算,不能拿正解的量去推 —— 这正是我在第 ② 步算错的那件事。

  2. ★★ 题面那两个部分分档,一个是盲区、一个是专门档 —— 10% 档(n = 3)替三个错法白送 10 分; 而 40% 档(帮贡全为 0)让第一次排序全部并列,把「漏输入序」从 4 顶到 300。 ⇒ 「题面上那几行数字,每一行都是一件工具」的又一次, ★ 而这次同一张表里两行的方向正好相反

  3. ★★★ 三个「触发 ≡ 抓获」一个不差,而第四个差 20 倍(都在档 0,n 随机 3~20):

    版本 第一层:满足触发条件的轮数 真被抓
    ✗ off-by-one n ≥ 6244 244 1.0
    ✗ 字典序 n ≥ 10186 186 1.0
    ✗ 等级反(档 1) 两位副帮主等级不同:299 299 1.0
    ✗ 漏输入序 n ≥ 1781 4 20.3 倍

    ⇒ ★★ 前三条能写成 ,是因为它们的触发条件只有一层,而且是一个整数; 最后那条有两层n ≥ 17 那一组里真有等级并列 sort 真把它换了过来)。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次。

  4. 冒泡版四档 1200 轮和正解逐字节相同,0 次不一致 —— 上面第 ⑤ 步那句话是量过的。

8★ 哪一版就已经能过了

p1786Count.cpp本页所有数字的出处
// 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 章那把尺子在这里的作用,恰恰是把注意力从算法上挪开: 三十秒算完「余量九万倍」,剩下的时间才够你把题面读第三遍。