题单 · 习题解析

洛谷 P1271 【深基9.例1】选举学生会

★★★ 「读入量大不大」的尺子是字节数不是数的个数:200 万个三位数比 110 万个十位数还少三成

原题:洛谷 P1271出自 第 10 章 排序:冒泡 → 归并 → 快排 的题单题面本地存档:2026-08-27
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

学校正在选举学生会成员,有 n1 ≤ n ≤ 999)名候选人,每名候选人编号分别从 1n, 现在收集到了 m1 ≤ m ≤ 2000000)张选票,每张选票都写了一个候选人编号。 现在想把这些堆积如山的选票按照投票数字从小到大排序。 设第 i1 ≤ i ≤ m)张选票上的数字为 aᵢ,则保证有 1 ≤ aᵢ ≤ n

输入格式

输入 nm 以及 m 个选票上的数字。

输出格式

求出排序后的选票编号。

输入输出样例

输入

5 10
2 5 2 2 5 2 2 2 1 2

输出

1 2 2 2 2 2 2 2 5 5

★ 注意题面给的两个范围:候选人只有 999 个,选票却有 200 万张。 「值域小、数据多」——这就是这道题的全部形状。

1★ 值域只有 999,那就不用比较排序

★ 计数排序:读的时候顺手数一下就完了
cnt[a_i]++            读的时候顺手数
for v = 1..n          按编号从小到大
    输出 cnt[v] 个 v

复杂度 O(n + m),而 sortO(m log m)log 2×10⁶ ≈ 21)。

⇒ 排序这一步根本不用「排」——值域小的时候,「数一数」就是排序。

p1271.cpp★ 这一版就能 AC
// P1271 选举学生会 —— ★ 这一版就能 AC:计数排序
//
// 题面给了两个数:候选人 `n <= 999`,选票 `m <= 2×10⁶`,而且每张票上的数在 [1, n] 里。
// ⇒ **值域只有 999,数据却有 200 万** —— 这正是计数排序的主场:
//
// cnt[a_i]++ 读的时候顺手数
// for v = 1..n 按编号从小到大
// 输出 cnt[v] 个 v
//
// 复杂度 `O(n + m)`,而 `sort` 是 `O(m log m)`(log 2×10⁶ ≈ 21)。
//
// ⚠ 但这道题真正的门槛不在排序上,在 **IO** 上:要读 200 万个数、再输出 200 万个数。
// ⇒ 见 p1271Read.cpp 那张表 —— 读法选错了,算法再快也没用。
#include <bits/stdc++.h>
using namespace std;
static int cnt[1005];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 0; i < m; i++) { int x; cin >> x; cnt[x]++; }
/* ⚠ 末尾那个换行不能靠「v == n 且是这一格的最后一个」来判 ——
没人投给最后一号候选人时 cnt[n] == 0,那个条件永远不成立,
整份输出就少了结尾的换行。⇒ 老老实实数第几个。
★ 这个 bug 我自己踩了:第一版拿 .strip() 比对拍,正好把它抹掉了,
是 check:viz 逐字节比才报出来的(900 轮里 307 轮不一致)。 */
int printed = 0;
for (int v = 1; v <= n; v++)
for (int k = 0; k < cnt[v]; k++) cout << v << " \n"[++printed == m];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1271Sort.cppsort 版(也能过)
// 另一版:老老实实读进来,`sort` 一遍
//
// 它**也能过** —— 这一页不是要证明「sort 不行」,而是要量清楚
// **在这道题的规模上,两者差多少**(见 p1271Count.cpp)。
//
// ★ 值得先想清楚的一件事:`sort` 是 `O(m log m)` = 2×10⁶ × 21 ≈ 4.2×10⁷ 次比较,
// 计数排序是 `O(n + m)` ≈ 2×10⁶ 次自增。**理论上差 20 倍。**
// ⚠ 可实测差不了那么多 —— 因为这道题的时间**大头在 IO 上**,不在排序上。
// ⇒ 这正是「复杂度更优不等于快多少」的又一个现场(第 7 章 P1873 那条)。
#include <bits/stdc++.h>
using namespace std;
static int a[2000006];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 0; i < m; i++) cin >> a[i];
sort(a, a + m);
for (int i = 0; i < m; i++) cout << a[i] << " \n"[i + 1 == m];
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★ 只量排序那一段:快 40 倍

p1271Count.cpp只量排序
// 只量「排序那一段」:计数排序 vs sort(把 IO 摘出去)
//
// 用法:./p1271Count [n] [m] 人话版
// ./p1271Count [n] [m] csv 给 check:viz 用
//
// ★ 为什么要把 IO 摘出去:这道题端到端的时间**大头在 IO 上**(见 p1271Read.cpp),
// 直接比两个程序的总时间,量到的主要是读写,不是排序。
// ⇒ 想知道「计数排序到底快多少」,就得只掐排序那一段。
//
// 两个数一起报:
// · **次数**:sort 是 O(m log m) ≈ m × 21,计数排序是 m 次自增 + n 次扫描;
// · **秒表**:同一份数组,两种排法各跑一次。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static int a[2000006], b[2000006], cnt[1005];
int main(int argc, char** argv) {
int n = (argc > 1) ? atoi(argv[1]) : 999;
int m = (argc > 2) ? atoi(argv[2]) : 2000000;
bool csv = (argc > 3 && string(argv[3]) == "csv");
mt19937 rng(20260827u);
for (int i = 0; i < m; i++) a[i] = (int)(rng() % (unsigned)n) + 1;
memcpy(b, a, sizeof(int) * (size_t)m);
auto t0 = steady_clock::now();
sort(b, b + m);
double msSort = duration<double, milli>(steady_clock::now() - t0).count();
t0 = steady_clock::now();
memset(cnt, 0, sizeof(cnt));
for (int i = 0; i < m; i++) cnt[a[i]]++;
int p = 0;
for (int v = 1; v <= n; v++) for (int k = 0; k < cnt[v]; k++) a[p++] = v;
double msCount = duration<double, milli>(steady_clock::now() - t0).count();
bool same = memcmp(a, b, sizeof(int) * (size_t)m) == 0;
int lg = 0; for (int x = m; x > 0; x >>= 1) lg++;
long long sortOps = (long long)m * lg;
long long countOps = (long long)m + n;
if (csv) {
printf("n,%d\nm,%d\nsame,%d\n", n, m, same ? 1 : 0);
printf("sortOps,%lld\ncountOps,%lld\nopsRatio,%lld\n", sortOps, countOps, sortOps / max(1LL, countOps));
printf("sortFaster,%d\n", (msSort < msCount) ? 1 : 0);
return 0;
}
printf("n = %d、m = %d,只量排序那一段:\n\n", n, m);
printf(" sort %8.1f 毫秒 ≈ m × log2(m) = %lld 次比较\n", msSort, sortOps);
printf(" 计数排序 %8.1f 毫秒 ≈ m + n = %lld 次自增\n", msCount, countOps);
printf("\n 两边结果%s。次数差 %lld 倍,秒表差 %.1f 倍。\n",
same ? "逐字节相同" : "居然不一样!", sortOps / max(1LL, countOps), msSort / max(0.001, msCount));
printf(" ⚠ 而这道题端到端的时间大头在 IO 上 —— 排序快 %.1f 倍,总时间未必快那么多。\n",
msSort / max(0.001, msCount));
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
n = 999m = 2×10⁶(只量排序,把 IO 摘出去) 次数 秒表
sort m × log₂m = 42 000 000 次比较 65.1 毫秒
计数排序 m + n = 2 000 999 次自增 1.6 毫秒
★ 这次两把尺子的关系反过来了:秒表差得比次数还多

次数差 20 倍,秒表差 40.5 倍 —— 秒表差得更多

平常是反过来的(次数差很多、秒表差得少,因为常数拉平了)。 这次之所以掉个个儿:计数排序的每一步更便宜 —— 顺序自增一个小数组,全在缓存里;而 sort 每一步是比较 + 交换,还要跳着访问。

第 45 章那条「顺序访问和随机访问能差 80 多倍」在这儿露了个头: 「一步」有多贵,本身就是个变量。

3⚠ 可端到端只快 1.8 倍 —— 大头根本不在排序上

p1271GenBig.cpp顶格数据
// 顶格数据(P1271 计时用):`./p1271GenBig <n> <m> [level]`
// level 0 随机 / 1 全投一个人 / 2 均匀分给每个人
// ⚠ 题面:n <= 999、m <= 2×10⁶、a_i ∈ [1, n]。默认按顶格。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
int n = (argc > 1) ? atoi(argv[1]) : 999;
int m = (argc > 2) ? atoi(argv[2]) : 2000000;
int level = (argc > 3) ? atoi(argv[3]) : 0;
mt19937 rng(20260827u);
printf("%d %d\n", n, m);
for (int i = 0; i < m; i++) {
int v = (level == 1) ? 1 : (level == 2) ? (i % n) + 1 : (int)(rng() % (unsigned)n) + 1;
printf("%d%c", v, i + 1 == m ? '\n' : ' ');
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

顶格数据(n = 999m = 2×10⁶端到端实测:

数据 计数排序 sort
随机票 90 毫秒 159 毫秒
全投一个人 82 毫秒 95 毫秒
⚠ 排序快 40 倍,端到端只快 1.8 倍

因为这道题的时间大头在读写上:要读 200 万个数,再输出 200 万个数。 排序那一段本来就只占几十毫秒,再快也省不出多少。

「复杂度更优」和「跑得快多少」是两个问题第 7 章 P1873 那条的又一面:那道题复杂度更优的版本反而更慢)。 ⇒ 想知道优化到底值不值,得先知道时间花在哪儿 —— 下一步就去量。

4★★★ 那就量读写:200 万进、200 万出

p1271Read.cpp量读和写
// P1271 的真正门槛:200 万个数进来、200 万个数出去
//
// 用法:./p1271Read [n] [m] 人话版
// ./p1271Read [n] [m] csv 只打 `键,值`,给 check:viz 用
// 默认就是题面顶格:n = 999、m = 2×10⁶。
//
// ★ 这一页和[第 8 章 P2249]那一页量的是同一件事,但结论**正好相反**:
// 那道题 110 万个整数,默认 `cin` 234 毫秒,离 1 秒还有四倍余量 ⇒ 提示是虚的;
// 这道题**读 200 万 + 写 200 万**,量出来是另一个样子。
//
// ⚠ 这份程序自己造数据写进临时文件,再 freopen 回来读 —— 不需要喂输入。
// ⚠ 「同步开着」那一趟必须排最前面(关掉同步之后 cin 会预读,换文件会读到残渣)。
//
// 输出那一侧量三种:① cout 同步开着 ② cout 关同步 ③ 自己拼一个大 buffer 一次 fwrite。
// ⚠ 输出统一写到 /dev/null,量的是**生成字节**的代价,不含磁盘。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static char path[64];
static int cnt[1005];
static int n, m;
static void makeData(int nn, int mm) {
snprintf(path, sizeof(path), "/tmp/p1271-bench-%d.txt", (int)getpid());
FILE* f = fopen(path, "w");
if (!f) { fprintf(stderr, "写不出临时文件\n"); exit(1); }
mt19937 rng(20260827u);
fprintf(f, "%d %d\n", nn, mm);
for (int i = 0; i < mm; i++) fprintf(f, "%d%c", (int)(rng() % (unsigned)nn) + 1, i + 1 == mm ? '\n' : ' ');
fclose(f);
}
static void reopenIn() { if (!freopen(path, "r", stdin)) { fprintf(stderr, "读不回临时文件\n"); exit(1); } }
static char ibuf[1 << 22];
static size_t ipos = 0, ilen = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return EOF; }
return ibuf[ipos++];
}
static inline int readIntFast() {
int c = gc(), x = 0;
while (c != EOF && (c < '0' || c > '9')) c = gc();
for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0');
return x;
}
int main(int argc, char** argv) {
int nn = (argc > 1) ? atoi(argv[1]) : 999;
int mm = (argc > 2) ? atoi(argv[2]) : 2000000;
bool csv = (argc > 3 && string(argv[3]) == "csv");
makeData(nn, mm);
double rms[4], wms[3];
long long sum[4];
/* ---- 读:四种 ---- */
{ reopenIn(); memset(cnt, 0, sizeof(cnt));
auto t0 = steady_clock::now();
cin >> n >> m; long long s = 0;
for (int i = 0; i < m; i++) { int x; cin >> x; cnt[x]++; s += x; }
sum[0] = s; rms[0] = duration<double, milli>(steady_clock::now() - t0).count(); }
{ reopenIn(); memset(cnt, 0, sizeof(cnt));
auto t0 = steady_clock::now();
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n >> m; long long s = 0;
for (int i = 0; i < m; i++) { int x; cin >> x; cnt[x]++; s += x; }
sum[1] = s; rms[1] = duration<double, milli>(steady_clock::now() - t0).count(); }
{ reopenIn(); memset(cnt, 0, sizeof(cnt));
auto t0 = steady_clock::now();
if (scanf("%d %d", &n, &m) != 2) { n = m = 0; }
long long s = 0;
for (int i = 0; i < m; i++) { int x; if (scanf("%d", &x) != 1) break; cnt[x]++; s += x; }
sum[2] = s; rms[2] = duration<double, milli>(steady_clock::now() - t0).count(); }
{ reopenIn(); ipos = ilen = 0; memset(cnt, 0, sizeof(cnt));
auto t0 = steady_clock::now();
n = readIntFast(); m = readIntFast();
long long s = 0;
for (int i = 0; i < m; i++) { int x = readIntFast(); cnt[x]++; s += x; }
sum[3] = s; rms[3] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ---- 写:三种(都写到 /dev/null,量生成字节的代价) ---- */
{ FILE* devnull = fopen("/dev/null", "w");
/* ① cout 同步开着 */
{ auto t0 = steady_clock::now();
ostringstream oss;
ios::sync_with_stdio(true);
for (int v = 1; v <= n; v++) for (int k = 0; k < cnt[v]; k++) oss << v << ' ';
fwrite(oss.str().data(), 1, oss.str().size(), devnull);
wms[0] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ② ostringstream(等价于 cout 关同步的代价) */
{ auto t0 = steady_clock::now();
ostringstream oss;
for (int v = 1; v <= n; v++) for (int k = 0; k < cnt[v]; k++) oss << v << ' ';
fwrite(oss.str().data(), 1, oss.str().size(), devnull);
wms[1] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ③ 自己拼 buffer 一次 fwrite */
{ auto t0 = steady_clock::now();
static char ob[1 << 25];
size_t p = 0;
for (int v = 1; v <= n; v++) {
char tmp[8]; int len = 0, x = v;
do { tmp[len++] = char('0' + x % 10); x /= 10; } while (x);
for (int k = 0; k < cnt[v]; k++) {
for (int t = len - 1; t >= 0; t--) ob[p++] = tmp[t];
ob[p++] = ' ';
}
}
fwrite(ob, 1, p, devnull);
wms[2] = duration<double, milli>(steady_clock::now() - t0).count(); }
fclose(devnull);
}
remove(path);
bool same = (sum[0] == sum[1] && sum[1] == sum[2] && sum[2] == sum[3]);
if (csv) {
const char* rk[4] = { "cin", "nosync", "scanf", "fast" };
for (int k = 0; k < 4; k++) printf("read_%s,%.1f\n", rk[k], rms[k]);
const char* wk[3] = { "coutSync", "oss", "buffer" };
for (int k = 0; k < 3; k++) printf("write_%s,%.1f\n", wk[k], wms[k]);
printf("same,%d\nchecksum,%lld\n", same ? 1 : 0, sum[0]);
printf("readTotalCin,%.1f\nreadTotalFast,%.1f\n", rms[0] + wms[0], rms[3] + wms[2]);
return 0;
}
const char* rname[4] = { "cin(默认,同步开着)", "cin + sync_with_stdio(false)", "scanf", "手写快读(fread)" };
const char* wname[3] = { "cout(同步开着)", "ostringstream(≈ 关同步的 cout)", "自己拼 buffer + 一次 fwrite" };
auto disp = [](const string& t) { int w = 0; for (unsigned char c : t) { if ((c & 0xC0) == 0x80) continue; w += (c < 0x80) ? 1 : 2; } return w; };
auto pad = [&](const string& t, int w) { return t + string(max(0, w - disp(t)), ' '); };
printf("n = %d、m = %d(读 %d 个整数,写 %d 个整数):\n\n", nn, mm, mm + 2, mm);
printf(" 读:\n");
for (int k = 0; k < 4; k++) printf(" %s %8.1f 毫秒\n", pad(rname[k], 32).c_str(), rms[k]);
printf("\n 写:\n");
for (int k = 0; k < 3; k++) printf(" %s %8.1f 毫秒\n", pad(wname[k], 32).c_str(), wms[k]);
printf("\n 四趟读出来的校验和%s。\n", same ? "完全一致" : "居然不一致!");
printf(" ⇒ 「最慢的读 + 最慢的写」= %.1f 毫秒,「最快 + 最快」= %.1f 毫秒。时限是 1 秒。\n",
rms[0] + wms[0], rms[3] + wms[2]);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
读(200 万个整数) 毫秒 写(200 万个整数) 毫秒
cin(默认,同步开着) 200.7 cout(同步开着) 45.1
cin + sync_with_stdio(false) 43.5 ostringstream(≈ 关同步) 44.0
scanf 63.7 自己拼 buffer + 一次 fwrite 3.9
手写快读(fread 10.6

⇒ 「最慢的读 + 最慢的写」= 245.7 毫秒,「最快 + 最快」= 14.5 毫秒。时限 1 秒。

★★★ 200 万个数,默认 cin 只要 200 毫秒 —— 比 110 万个数的那道题还快

第 8 章 P2249 那页量过同一件事:110 万个整数,默认 cin 234 毫秒。 这道题是 200 万个,cin 却只要 200.7 毫秒

数的个数多了 1.82 倍,时间反而少了。为什么?——因为尺子拿错了

数的个数 值域 文件大小 平均每个数
P2249 顶格 110 万 10⁹(十位数) 10.88 MB 9.9 字节
本题顶格 200 万 999(三位数) 7.78 MB 3.9 字节

个数多 1.82 倍,字节数只有 0.72 倍。

⇒ ★★★ 「读入量大不大」的尺子是字节数,不是数的个数。 判断要不要读入优化时,先把「几个数 × 每个数几位」乘出来 —— 2×10⁷ 个十位数(第 6 章 P2367,连 scanf 都不够)是 200 MB 那个量级, 和这里的 7.78 MB 差着三十倍。

5★ 对拍

p1271Gen.cpp生成器:三个档位
// 数据生成器(P1271 对拍用):`./p1271Gen <seed> [level]`
//
// level 0(默认)随机票
// level 1 ★ **票全投给同一个人** —— 计数排序里 cnt 只有一格非零
// level 2 ★ **每人恰好一票**(m = n)—— 另一头的极端
//
// ⚠ 题面:1 <= n <= 999,1 <= m <= 2×10⁶,1 <= a_i <= n。
// 小数据对拍只验「答案对不对」;这道题真正要量的是**规模**,那归 genBig.cpp。
#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)); }
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, 8);
int m = (level == 2) ? n : ri(1, 20);
printf("%d %d\n", n, m);
for (int i = 0; i < m; i++) {
int v;
if (level == 1) v = 1;
else if (level == 2) v = i + 1;
else v = ri(1, n);
printf("%d%c", v, i + 1 == m ? '\n' : ' ');
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
档位 计数排序 ≡ sort
level 0 随机票 300 / 300
level 1 全投一个人 300 / 300
level 2 每人恰好一票 300 / 300

⇒ 900 轮逐字节相同。★ 这道题的两条路都对,对拍要验的不是「谁错了」, 而是「换了一条完全不同的路,答案还是同一个」—— 计数排序连一次比较都没做过,却排出了和 sort 一模一样的序列。

6一张总表

版本 排序那一段 端到端(顶格随机) 结果
p1271Sort 65.1 毫秒(4.2×10⁷ 次比较) 159 毫秒 AC
p1271 1.6 毫秒(2×10⁶ 次自增) 90 毫秒 AC
这一页记住三句话
  1. ★★ 值域小的时候,「数一数」就是排序。 n ≤ 999m 到 200 万 —— 计数排序 O(n + m),一次比较都不用做。
  2. ★★★ 「读入量大不大」的尺子是字节数,不是数的个数。 200 万个三位数(7.78 MB)比 110 万个十位数(10.88 MB)还少三成, 所以默认 cin 反而更快。
  3. 优化之前先量时间花在哪儿。 这道题排序快 40 倍,端到端只快 1.8 倍 —— 因为大头在读写上。