0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1068,日期见页头。两边不一致时信原站。
题目描述
世博会志愿者的选拔工作正在 A 市如火如荼的进行。为了选拔最合适的人才,A 市对所有报名的选手
进行了笔试,笔试分数达到面试分数线的选手方可进入面试。面试分数线根据计划录取人数的 150% 划定,
即如果计划录取 m 名志愿者,则面试分数线为排名第 m × 150%(向下取整)名的选手的分数,
而最终进入面试的选手为笔试成绩不低于面试分数线的所有选手。
现在就请你编写程序划定面试分数线,并输出所有进入面试的选手的报名号和笔试成绩。
输入格式
第一行,两个整数 n, m(5 ≤ n ≤ 5000,3 ≤ m ≤ n),中间用一个空格隔开,
其中 n 表示报名参加笔试的选手总数,m 表示计划录取的志愿者人数。
输入数据保证 m × 150% 向下取整后小于等于 n。
第二行到第 n+1 行,每行包括两个整数,中间用一个空格隔开,分别是选手的报名号 k
(1000 ≤ k ≤ 9999)和该选手的笔试成绩 s(1 ≤ s ≤ 100)。数据保证选手的报名号各不相同。
输出格式
第一行,有 2 个整数,用一个空格隔开,第一个整数表示面试分数线;第二个整数为进入面试的选手的
实际人数。
从第二行开始,每行包含 2 个整数,中间用一个空格隔开,分别表示进入面试的选手的报名号和笔试成绩,
按照笔试成绩从高到低输出,如果成绩相同,则按报名号由小到大的顺序输出。
说明 / 提示
【样例说明】m × 150% = 3 × 150% = 4.5,向下取整后为 4。保证 4 个人进入面试的分数线为 88,
但因为 88 有重分,所以所有成绩大于等于 88 的选手都可以进入面试,故最终有 5 个人进入面试。
NOIP 2009 普及组 第二题。
输入输出样例
输入
6 3 1000 90 3239 88 2390 95 7231 84 1005 95 1001 88
输出
88 5 1005 95 2390 95 1000 90 1001 88 3239 88
★ 样例说明里那段话就是这道题的全部难点 —— 算出来 4 个名额, 可第 4 名那个 88 分有并列,于是最后进去 5 个人。
1★ 这道题的难点全写在样例说明里
① 分数线 = 排名第 ⌊m × 150%⌋ 名那个人的分数;
② 最终进面试的是所有分数 ≥ 分数线的人 —— 人数可能比 ⌊1.5m⌋ 多;
③ 排序是双关键字:成绩从高到低,成绩相同时报名号从小到大。
⇒ 第 ② 条是全部难点,而它在题面和样例说明里各写了一遍。 (第 8 章 P2249 那页说过「第一步永远是跑样例」—— 这道题还要再加一句:样例说明也要读完。)
// P1068 分数线划定 —— ★ 这一版就能 AC//// 三件事,缺一不可:// ① 分数线 = 排名第 `⌊m × 150%⌋` 名那个人的**分数**;// ② 最终进面试的是**所有分数 >= 分数线**的人 —— 人数可能比 ⌊1.5m⌋ **多**// (样例就是:算出来 4 个,因为第 4 名那个 88 分有并列,最后 5 个人进);// ③ 排序是**双关键字**:成绩从高到低,成绩相同时报名号从小到大。//// ⚠ 第 ② 条是这道题的全部难点,而它在样例里就写着 —— **样例说明专门解释了这件事**。// ⇒ 又一次「第一步永远是把样例读完」(第 8 章 P2249 那页那条)。//// ★ `m * 3 / 2` 和 `(int)(m * 1.5)` 在这道题上**完全等价**,一次都不会差// —— 网上常见的那条「别用 1.5,会掉精度」的提醒,主语错了,见 p1068Count.cpp。
#include <bits/stdc++.h>using namespace std;
struct Player { int id, score; };
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<Player> v(n); for (int i = 0; i < n; i++) cin >> v[i].id >> v[i].score;
sort(v.begin(), v.end(), [](const Player& a, const Player& b) { if (a.score != b.score) return a.score > b.score; // 成绩高的在前 return a.id < b.id; // ③ 成绩相同,报名号小的在前 });
int line = v[m * 3 / 2 - 1].score; // ① 第 ⌊1.5m⌋ 名的分数(下标从 0 起) int cnt = 0; while (cnt < n && v[cnt].score >= line) cnt++; // ② 所有 >= 分数线的都算
cout << line << ' ' << cnt << '\n'; for (int i = 0; i < cnt; i++) cout << v[i].id << ' ' << v[i].score << '\n'; return 0;}点「运行 ▶」看结果
2第一个 WA:只取前 ⌊1.5m⌋ 名
// ⚠ 故意写错的:只取前 ⌊m × 150%⌋ 名,没管并列//// int cnt = m * 3 / 2; ← 正解还要往后走,把所有「分数 >= 分数线」的都收进来//// ★ 它**样例就挂**:样例算出来分数线是 88、名额 4 个,// 可 88 分有两个人(1001 和 3239),题面明说这两个都进 ⇒ 正确答案是 **5** 个人。//// ⚠ 值得注意的是它错得**很轻**:分数线那个数是对的,前 4 行也一字不差,// 只是少了最后一行、而且人数少了 1。// ⇒ 这种「只差一行」的错,肉眼扫一遍输出很容易放过去 —— 逐字节比才看得出来。
#include <bits/stdc++.h>using namespace std;struct Player { int id, score; };
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<Player> v(n); for (int i = 0; i < n; i++) cin >> v[i].id >> v[i].score; sort(v.begin(), v.end(), [](const Player& a, const Player& b) { if (a.score != b.score) return a.score > b.score; return a.id < b.id; }); int cnt = m * 3 / 2; // ⚠ 就是这一行 int line = v[cnt - 1].score; cout << line << ' ' << cnt << '\n'; for (int i = 0; i < cnt; i++) cout << v[i].id << ' ' << v[i].score << '\n'; return 0;}点「运行 ▶」看结果
它输出 88 4 和 4 行,正确答案是 88 5 和 5 行。
★ 注意它错得多轻:分数线那个数是对的,前 4 行一字不差, 只是人数少了 1、少了最后一行。 ⇒ 这种错,肉眼扫一眼输出很容易放过去 —— 逐字节比才看得出来。
3第二个 WA:cmp 漏了第二关键字
// ⚠ 故意写错的:cmp 只写了成绩,漏掉第二关键字(报名号)//// return a.score > b.score; ← 成绩相同时,谁在前面就**不确定**了//// ★ 这一版的诡异之处:它**不一定错**。`std::sort` 不保证稳定,// 但也没规定并列的一定会乱 —— 数据小的时候常常「碰巧」是对的。// ⇒ 于是它是那种「本地样例过了、交上去 WA」的典型。//// ⚠ 更要命的是:**换一台机器、换一个编译器版本,它的输出可能就变了**// (`sort` 的实现细节不同)。⇒ 这类 bug 连「复现」都不保证。//// ★ 抓它要靠**大量同分**的数据(见 p1068Gen.cpp 的档位 1)。
#include <bits/stdc++.h>using namespace std;struct Player { int id, score; };
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<Player> v(n); for (int i = 0; i < n; i++) cin >> v[i].id >> v[i].score; sort(v.begin(), v.end(), [](const Player& a, const Player& b) { return a.score > b.score; // ⚠ 少了 id 那一句 }); int line = v[m * 3 / 2 - 1].score; int cnt = 0; while (cnt < n && v[cnt].score >= line) cnt++; cout << line << ' ' << cnt << '\n'; for (int i = 0; i < cnt; i++) cout << v[i].id << ' ' << v[i].score << '\n'; return 0;}点「运行 ▶」看结果
std::sort 不保证稳定 —— 成绩相同的两个人谁排前面,标准没规定。
本页这一版在样例上把两对并列都排反了,但那是这台机器这个标准库的行为:
换个编译器版本、换个数据规模,输出可能就变了。
⇒ 「本地样例过了、交上去 WA」的典型;更糟的是它连复现都不保证。
★ 所以这一页的断言只钉「和正解不一致」,不钉它具体输出了什么
(第 9 章 P1182 那页对 int 溢出也是这么处理的:
实现相关的具体值不写进断言)。
4★★★ 顺便查一条广为流传的提醒:「别用 1.5,会掉精度」
网上题解里常见这么一句:m × 150% 要写成 m * 3 / 2,别写 (int)(m * 1.5),浮点会掉精度。
听起来很有道理。扫一遍看看:
// 「别用 1.5,会掉精度」—— 这条提醒到底对不对?扫一遍//// 用法:./p1068Count 人话版// ./p1068Count csv 只打 `键,值`,给 check:viz 用//// 网上题解常见的提醒是:「`m * 150%` 要写成 `m * 3 / 2`,别写 `(int)(m * 1.5)`,// 浮点会掉精度」。听起来很有道理 —— **但这道题上它一次都不会错**。//// 这份程序做两件事:// ① 在题面的范围 `m ∈ [3, 5000]` 里,把三种写法逐个比过去;// ② 换几个别的倍数做对照,看「掉精度」到底什么时候真的发生。//// ★ 结论先写在这儿(下面是跑出来的):**危险的不是「用了浮点」,// 是「那个小数在二进制里做乘法之后,会不会正好跨过一个整数边界」** ——// 而这既不等于「是不是有限二进制小数」,也不能靠直觉,只能算或者扫。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 题面范围内的三种写法 */ int bad = 0, firstBad = -1; for (int m = 3; m <= 5000; m++) { int a = (int)(m * 1.5), b = m * 3 / 2, c = m * 150 / 100; if (a != b || b != c) { bad++; if (firstBad < 0) firstBad = m; } }
/* ② 对照:几个不同的倍数,各在 [1, 10⁶] 里错多少次 */ struct Case { const char* name; double f; long long num, den; }; Case cs[] = { { "1.5", 1.5, 3, 2 }, { "1.25", 1.25, 5, 4 }, { "1.1", 1.1, 11, 10 }, { "0.9", 0.9, 9, 10 }, { "0.7", 0.7, 7, 10 }, }; long long cnt[5]; long long first[5]; for (int i = 0; i < 5; i++) { cnt[i] = 0; first[i] = -1; for (long long m = 1; m <= 1000000; m++) { long long x = (long long)(m * cs[i].f), y = m * cs[i].num / cs[i].den; if (x != y) { cnt[i]++; if (first[i] < 0) first[i] = m; } } }
if (csv) { printf("inRange,%d\n", bad); for (int i = 0; i < 5; i++) printf("bad_%s,%lld\nfirst_%s,%lld\n", cs[i].name, cnt[i], cs[i].name, first[i]); return 0; } printf("① 题面范围 m ∈ [3, 5000]:(int)(m*1.5) / m*3/2 / m*150/100 三者不一致 **%d** 次\n", bad); printf(" ⇒ 那条「别用 1.5」的提醒,在这道题上**一次都用不上**。\n\n"); printf("② 换几个倍数,在 m ∈ [1, 10⁶] 里各错多少次:\n\n"); for (int i = 0; i < 5; i++) { printf(" ×%-5s 不一致 %8lld 次", cs[i].name, cnt[i]); if (first[i] > 0) printf(",第一个 m = %lld", first[i]); printf("\n"); } printf("\n ★ 1.5 和 1.25 是**二进制精确**的(3/2、5/4)—— 乘出来一位不差,当然不会错。\n"); printf(" ★★ 可 1.1 和 0.9 **并不精确**,却也一次没错 —— 「不精确」不等于「会出事」。\n"); printf(" ★★★ 只有 0.7 真的错了,而且从 m = %lld 就开始。\n", first[4]); printf(" ⇒ 判断依据不是「是不是浮点」,也不是「精不精确」,\n"); printf(" 是「乘完之后会不会正好落在整数边界的另一侧」—— 这只能算,或者像这样扫一遍。\n"); return 0;}点「运行 ▶」看结果
题面范围 m ∈ [3, 5000],三种写法 (int)(m*1.5) / m*3/2 / m*150/100
逐个比过去,不一致 0 次。
原因很干脆:1.5 在二进制里是精确的(就是 3/2,等于「加一半」),
只要 3m 没超过 2⁵³,乘出来一位不差。
★★ 可事情还有另一半 —— 换几个倍数在 m ∈ [1, 10⁶] 里扫:
| 倍数 | 二进制里精确吗 | 取整不一致的次数 |
|---|---|---|
×1.5(3/2) |
精确 | 0 |
×1.25(5/4) |
精确 | 0 |
×1.1(11/10) |
⚠ 不精确 | 0 |
×0.9(9/10) |
⚠ 不精确 | 0 |
×0.7(7/10) |
⚠ 不精确 | ★ 18 719,第一个 m = 90 |
1.1 和 0.9 都不精确,却一次都没错。
⇒ 所以判断依据既不是「是不是用了浮点」,也不是「那个小数精不精确」, 而是「乘完之后会不会正好落在整数边界的另一侧」—— 这件事只能算,或者像上面这样扫一遍。
★ 一般化:别把「听起来有道理的提醒」当结论用。
它可能是对的(0.7 就是反例),但它的适用范围往往比说的窄得多。
5★ 对拍:三个档位
// P1068 的对拍参照物:不用 sort,用**选择排序**自己排一遍//// ★ 它和正解**没有共用任何一行排序代码** —— 正解用 `std::sort` + 自定义 cmp,// 这里是最笨的双重循环,每次挑出「成绩最高、并列时报名号最小」的那个。// ⇒ 两边一起错的概率极低(第 7 章 P1147 那条:验算要走一条无关的路)。//// ⚠ 顺带:选择排序**不稳定**,但这里根本不需要稳定 ——// 因为 cmp 已经把「成绩 + 报名号」定死了,任何两个人都分得出先后,// 排出来的顺序是**唯一**的。⇒ 「要不要稳定排序」这个问题,// 在「关键字能把所有元素两两分开」时根本不存在(P1104 那页才是它真的要紧的地方)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<int> id(n), sc(n); for (int i = 0; i < n; i++) cin >> id[i] >> sc[i]; vector<bool> used(n, false); vector<int> ord; for (int k = 0; k < n; k++) { int best = -1; for (int i = 0; i < n; i++) { if (used[i]) continue; if (best < 0 || sc[i] > sc[best] || (sc[i] == sc[best] && id[i] < id[best])) best = i; } used[best] = true; ord.push_back(best); } int line = sc[ord[m * 3 / 2 - 1]]; int cnt = 0; while (cnt < n && sc[ord[cnt]] >= line) cnt++; cout << line << ' ' << cnt << '\n'; for (int i = 0; i < cnt; i++) cout << id[ord[i]] << ' ' << sc[ord[i]] << '\n'; return 0;}点「运行 ▶」看结果
// 数据生成器(P1068 对拍用):`./p1068Gen <seed> [level]`//// level 0(默认)成绩随机取 [1,100] —— 兜底// level 1 ★ **大量同分**:成绩只在 3 个值里取// ⇒ 专抓 p1068NoTie(cmp 漏了报名号)和 p1068Cut(没算并列)// level 2 ★ **分数线上正好有一堆并列**:先随机造,再把第 ⌊1.5m⌋ 名前后的分数抹平// ⇒ 专抓 p1068Cut//// ⚠ 题面:5 <= n <= 5000,3 <= m <= n,报名号 k ∈ [1000, 9999] 且**互不相同**,// 成绩 s ∈ [1, 100],并且保证 ⌊1.5m⌋ <= n。// ★ 报名号互不相同这一条必须守住 —— 否则「并列时按报名号排」就没有唯一答案了,// 两个程序会「一致地输出垃圾」。
#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(5, 30); int m = ri(3, max(3, (n * 2) / 3)); // 保证 ⌊1.5m⌋ <= n while (m * 3 / 2 > n) m--;
/* 报名号互不相同 */ vector<int> ids; { set<int> s; while ((int)s.size() < n) s.insert(ri(1000, 9999)); ids.assign(s.begin(), s.end()); for (int i = n - 1; i > 0; i--) swap(ids[i], ids[ri(0, i)]); } vector<int> sc(n); for (int i = 0; i < n; i++) sc[i] = (level == 1) ? ri(1, 3) * 10 : ri(1, 100); if (level == 2) { /* 把分数线附近抹平:先排个序找出第 ⌊1.5m⌋ 名的分数,再把一批人都设成它 */ vector<int> t = sc; sort(t.rbegin(), t.rend()); int line = t[m * 3 / 2 - 1]; for (int i = 0; i < n; i++) if (ri(0, 2) == 0) sc[i] = line; } printf("%d %d\n", n, m); for (int i = 0; i < n; i++) printf("%d %d\n", ids[i], sc[i]); return 0;}点「运行 ▶」看结果
每档 300 轮,和参照物不一致的轮数:
| 档位 | 正解 ≡ 参照物 | ⚠ Cut 没算并列 |
⚠ NoTie 漏关键字 |
|---|---|---|---|
level 0 成绩随机取 [1,100] |
300 / 300 | 32 | 117 |
level 1 ★ 成绩只有 3 个值 |
300 / 300 | 230 | 297 |
level 2 分数线上一堆并列 |
300 / 300 | 228 | 282 |
level 0 照题面随机(成绩 1~100),Cut 只抓 32/300 ——
因为 100 种成绩、几十个人,第 ⌊1.5m⌋ 名正好有并列的概率不高。
把成绩压到只有 3 个值,抓获数直接跳到 230/300。
⇒ 和第 7 章 P1102 那条是同一件事:
抓不抓得到,看的是「人数 / 值域」的比值,不是「数据大不大」。
★ 有意思的是 level 2(专门在分数线上造并列)并不比 level 1 更强(228 vs 230)——
把值域压小这个「笨办法」,效果和「精心构造」一样好。
6一张总表
| 版本 | 错在哪 | 样例 | 对拍(900 轮) | 结果 |
|---|---|---|---|---|
p1068 |
—— | ✓ | 900 / 900 | ★ AC |
⚠ p1068Cut |
没算分数线上的并列 | ✗ 挂 | 490 | ✗ WA |
⚠ p1068NoTie |
cmp 漏了报名号 | ✗ 挂 | 696 | ✗ WA(而且不确定) |
- ★★ 样例说明也是题面。 这道题的全部难点(分数线上的并列要全进) 在题面和样例说明里各写了一遍,两处都跳过去才会写出第 ② 步那一版。
- ★★★ 别把「听起来有道理的提醒」当结论用。
「别用 1.5 会掉精度」在这道题上一次都用不上(
1.5是二进制精确的); 而真正会出事的0.7,从m = 90就开始错。判断依据只能靠算或者扫。 - ★ 实现相关的错,只钉「不一致」,不钉具体输出。
sort对并列元素的顺序标准没规定 —— 换个编译器它可能就变了。