0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1042,日期见页头。两边不一致时信原站。
题目背景
国际乒联现在主席沙拉拉自从上任以来就立志于推行一系列改革,以推动乒乓球运动在全球的普及。
其中 11 分制改革引起了很大的争议,有一部分球员因为无法适应新规则只能选择退役。
华华就是其中一位,他退役之后走上了乒乓球研究工作,
意图弄明白 11 分制和 21 分制对选手的不同影响。
题目描述
华华通过以下方式进行分析,首先将比赛每个球的胜负列成一张表,
然后分别计算在 11 分制和 21 分制下,双方的比赛结果(截至记录末尾)。
比如现在有这么一份记录(其中 W 表示华华获得一分,L 表示华华对手获得一分):
WWWWWWWWWWWWWWWWWWWWWWLW
在 11 分制下,此时比赛的结果是华华第一局 11 比 0 获胜,第二局 11 比 0 获胜,
正在进行第三局,当前比分 1 比 1。而在 21 分制下,此时比赛结果是华华第一局 21 比 0
获胜,正在进行第二局,比分 2 比 1。如果一局比赛刚开始,则此时比分为 0 比 0。
直到分差大于或者等于 2,才一局结束。
注意:当一局比赛结束后,下一局立刻开始。
你的程序就是要对于一系列比赛信息的输入(WL 形式),输出正确的结果。
输入格式:每个输入文件包含若干行字符串,字符串由大写的 W、L 和 E 组成。
其中 E 表示比赛信息结束,程序应该忽略 E 之后的所有内容。
输出格式:输出由两部分组成,每部分有若干行,每一行对应一局比赛的比分(按比赛信息输入顺序)。
其中第一部分是 11 分制下的结果,第二部分是 21 分制下的结果,两部分之间由一个空行分隔。
数据范围:每行至多 25 个字母,最多有 2500 行。
【管理员附注】 本题为非常早期的试题,在测试点中出现了如下两个问题。在洛谷上这两个测试点的疏漏被保留:
- 有一个测试点实际上有
2501行数据。 - 有一个测试点的输入数据出现了非
W、L和E的字符,不符合输入格式的要求。 不过这些字符只出现在E字符之后,按题目要求忽略E之后的全部字符即可。
来源:NOIP 2003 普及组第一题。
输入输出样例
输入
WWWWWWWWWWWWWWWWWWWW WWLWE
输出
11:0 11:0 1:1 21:0 2:1
两部分之间有一个空行。上面那段输出是仓库里的 p1042.cpp 真跑出来的。
1先说结论:这一页有三个错版,样例一个都挡不住
这道题没有算法,全部分数在读题上。下面三个版本各错一处, 而它们跑样例的输出和正解逐字节相同:
① 忘了「分差 >= 2」 <- 样例里从没出现过 10:10
② 末尾那一局是 0:0 就不输出 <- 样例末尾是 1:1 和 2:1
③ 按题面「最多 2500 行」读 <- 样例只有 2 行
⇒ 所以这一页的做法反过来:先把三处坑一个个摆出来,最后用对拍量一量各自有多难抓。
2第 ① 版:谁先到 11 分谁赢(漏了半句话)
// P1042 的第 ① 版:「谁先到 11 分谁赢」—— 大多数人真实的第一反应//// ⚠ 它漏掉了题面里那半句:**「直到分差大于或者等于 2,才一局结束」**。// 10:10 之后要一直打到领先两分(11:10 不算完,12:10 才算)。//// ★★ 而这一版**样例是过的** —— 样例那串 WWWW…WWLW 里根本没出现过 10:10,// 分差那条规则从头到尾没被用到一次。⇒ 这是「样例挡不住」的典型。
#include <bits/stdc++.h>using namespace std;
void play(const string& s, int target) { int a = 0, b = 0; for (char c : s) { (c == 'W' ? a : b)++; if (a >= target || b >= target) { // ⚠ 少了 && abs(a - b) >= 2 printf("%d:%d\n", a, b); a = b = 0; } } printf("%d:%d\n", a, b);}
int main() { string s; char c; while (cin >> c) { if (c == 'E') break; if (c == 'W' || c == 'L') s += c; } play(s, 11); printf("\n"); play(s, 21); return 0;}点「运行 ▶」看结果
题面那半句是:「直到分差大于或者等于 2,才一局结束」。
10:10 之后 11:10 不算完,要打到 12:10。
样例那串是 WWWW…WWLW —— 华华一路 22 分不失误。
10:10 这种局面在样例里根本没机会出现,所以分差那条规则一次都没被用到。
★ 一条规则在样例里没被触发过,样例就完全没有验它。 ⇒ 读题时值得多做一步:把题面里每一条规则圈出来,逐条问「样例的哪一行验了它」。 问不出来的那几条,就是你必须自己造数据的地方。
3第 ② 版:分差补上了,但末尾那一行 0:0 被吞了
和正解的差别只有一个 if:
if (a || b) printf("%d:%d\n", a, b); // ← 这一版
printf("%d:%d\n", a, b); // ← 正解
// P1042 的第 ② 版:分差补上了,但「最后那一局是 0:0」被吞了//// 差别只有一个 if:// if (a || b) printf("%d:%d\n", a, b); <- 这一版// printf("%d:%d\n", a, b); <- 正解//// ⚠ 「一局刚好在记录末尾打完」时,新的一局**已经开始了**(题面:下一局立刻开始),// 而它的比分是 0:0 —— 题面专门写了一句「如果一局比赛刚开始,则此时比分为 0 比 0」。// ★ 直觉上「0:0 有什么好输出的」,可它是题目明说要的一行。//// ★★ 样例同样挡不住:样例末尾停在 1:1 和 2:1,两种赛制都不是 0:0。
#include <bits/stdc++.h>using namespace std;
void play(const string& s, int target) { int a = 0, b = 0; for (char c : s) { (c == 'W' ? a : b)++; if ((a >= target || b >= target) && abs(a - b) >= 2) { printf("%d:%d\n", a, b); a = b = 0; } } if (a || b) printf("%d:%d\n", a, b); // ⚠ 就是这个 if}
int main() { string s; char c; while (cin >> c) { if (c == 'E') break; if (c == 'W' || c == 'L') s += c; } play(s, 11); printf("\n"); play(s, 21); return 0;}点「运行 ▶」看结果
题面写了两句话,得合起来读:
- 「当一局比赛结束后,下一局立刻开始」
- 「如果一局比赛刚开始,则此时比分为
0比0」
⇒ 记录正好在一局打完时结束时,新的一局已经开始了,它的比分是 0:0,要打出来。
直觉上「0:0 有什么好输出的」,可它是题目明说要的一行。
★ 顺带一句:11 分制和 21 分制各自判断这件事。
一份记录可能是「11 分制正好打完、21 分制打了一半」,两部分的末尾行互不相干。
4★ 正解:两种赛制只差一个数字 —— 那就只写一遍
// P1042 乒乓球(NOIP 2003 普及组)—— 能 AC 的那一版//// ★ 这一版真正的「关键的一步」不是算法(这题没有算法),而是**把两种赛制写成一个函数**:// 11 分制和 21 分制只差一个数字,抄两遍就等于给自己造两处能各自写错的地方。// ⇒ 一个参数 target,两次调用,两边永远一致。//// 三条规则,一条都不能少:// ① 一局结束 = 有人到了 target **并且** 分差 >= 2 <- ⚠ 后半句样例挡不住// ② 一局结束后下一局立刻开始// ③ 记录末尾那一局**没打完也要输出**,哪怕它是 0:0 <- ⚠ 这条样例也挡不住
#include <bits/stdc++.h>using namespace std;
void play(const string& s, int target) { int a = 0, b = 0; for (char c : s) { (c == 'W' ? a : b)++; if ((a >= target || b >= target) && abs(a - b) >= 2) { printf("%d:%d\n", a, b); a = b = 0; // 下一局立刻开始 } } printf("%d:%d\n", a, b); // ★ 末尾那一局,哪怕 0:0 也要打}
int main() { string s; char c; while (cin >> c) { // ★ >> 会自动跳过换行,读到 E 收工 if (c == 'E') break; if (c == 'W' || c == 'L') s += c; } play(s, 11); printf("\n"); // 两部分之间空一行 play(s, 21); return 0;}点「运行 ▶」看结果
11 分制和 21 分制的逻辑一模一样,只差一个 target。
抄两遍等于给自己造了两处能各自写错的地方 —— 而且改的时候只改一处、忘了另一处,
是这类模拟题最常见的丢分方式。
void play(const string& s, int target) { … } // 写一遍
play(s, 11); printf("\n"); play(s, 21); // 用两次⇒ 第 47 章那条又出现了: 「关键的一步」可以是「换一种写法,让一整类坑消失」。 这里消失的是「两份复制粘贴的代码从此不一致」。
char c;
while (cin >> c) { if (c == 'E') break; if (c == 'W' || c == 'L') s += c; }cin >> c 自动跳过所有空白(换行、空格),所以「若干行」这件事根本不用管;
读到 E 就收工,E 之后的全部内容(包括管理员附注里说的那些非法字符)自然被忽略。
5第 ③ 版:算法全对,栽在「按题面开上界」
题面写着「最多有 2500 行」,于是按行读、读满 2500 行收工 ——
开一个 char buf[2500][30] 是同一个错法(那个还会越界,更难查)。
而管理员附注白纸黑字写着:「有一个测试点实际上有 2501 行数据。」
这道题是 2003 年的老题,数据本身有疏漏,而洛谷选择保留它。 下次遇到的可能是别的数、别的题,所以要带走的是那个习惯:
读到没有为止(while (cin >> …)),别拿题面给的上界当循环边界。
数组开大一点(乘 2、加 10)也是同一条:上界写错了,你就跟着错。
6★★★ 对拍:三个错版,三种「有多难抓」
// 数据生成器(P1042 对拍用):`./p1042Gen <seed> [level]`//// level 0(默认)顺手写法:随机 1~40 行,每行 1~25 个 W/L,末尾一行 E// level 1 ★ 专造「记录正好在一局打完时结束」—— 逼出那一行 0:0// level 2 ★ 2501 行(题面说最多 2500 行,而洛谷的管理员附注说有一个测试点是 2501 行)//// ★ 三个档位各自抓的是不同的错版,正文第 ⑤ 步有那张表。// ⚠ 注意 level 0 **不是没用**:忘了「分差 >= 2」的那一版它一抓一个准// (随机 50/50 的球,10:10 平局经常出现)。真正抓不到的是另外两个错版。
#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) { unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1; int level = (argc > 2) ? atoi(argv[2]) : 0; rng.seed(seed);
string balls; if (level == 1) { // 一直发球,直到 11 分制下**正好**打完一局就收手 ⇒ 正解要在末尾补一行 0:0 int a = 0, b = 0; while (true) { char c = (rng() & 1) ? 'W' : 'L'; balls += c; (c == 'W' ? a : b)++; if ((a >= 11 || b >= 11) && abs(a - b) >= 2) break; } } else { int lines = (level == 2) ? 2501 : ri(1, 40); for (int i = 0; i < lines; i++) { int len = (level == 2) ? ri(1, 3) : ri(1, 25); for (int j = 0; j < len; j++) balls += (rng() & 1) ? 'W' : 'L'; balls += '\n'; } }
// 按每行至多 25 个字母切开输出 int col = 0; for (char c : balls) { if (c == '\n') { if (col) { putchar('\n'); col = 0; } continue; } putchar(c); if (++col == 25) { putchar('\n'); col = 0; } } if (col) putchar('\n'); printf("E\n"); return 0;}点「运行 ▶」看结果
本机实测(每档 300 轮,check:viz 每次都真跑一遍):
| 生成器 | ① 忘分差 | ② 吞 0:0 | ③ 只读 2500 行 |
|---|---|---|---|
| 档位 0:顺手随机 1~40 行 | 248 / 300 | 21 / 300 | ★ 0 / 300 |
| 档位 1:逼出「正好打完」 | 53 / 300 | ★ 300 / 300 | 0 / 300 |
| 档位 2:2501 行 | 300 / 300 | 25 / 300 | ★ 300 / 300 |
- ① 忘分差:顺手就抓得到(248 / 300)。 随机 50/50 的球,
10:10天天有。 - ② 吞 0:0:顺手 300 轮只抓到 21 次(7%)。 记录要正好停在一局打完的那一拍上, 概率大约是「一局多少个球」的倒数。⇒ 这是概率低,多跑几轮总会撞上。
- ★★★ ③ 只读 2500 行:顺手 300 轮,一次都抓不到 —— 而且再跑三万轮也一样。 因为顺手生成器造的是「1~40 行」,它的取值范围根本够不着 2501 那条线。 这不是运气不好,是结构上不可能。
⇒ 「对拍 300 轮全绿」到底有多强,完全取决于生成器造得出什么。 判断办法很朴素:把每个错版的触发条件写下来,逐条问「我的生成器能造出这个条件吗」。 答不上来的那一条,就得单开一个档位。 (第 52 章那次是生成器亲手删掉了一个边界;这一次是它的上界压根不够。)
7四个版本并排
| 版本 | 错在哪 | 样例 | 顺手对拍 300 轮 | 能过吗 |
|---|---|---|---|---|
① p1042Naive |
少了 分差 >= 2 |
✓ 一字不差 | 被抓 248 轮 | ✗ |
② p1042NoTail |
末尾 0:0 不输出 |
✓ 一字不差 | 被抓 21 轮 | ✗ |
③ p1042Limit |
只读 2500 行 | ✓ 一字不差 | ★ 一轮都没抓到 | ✗ |
④ p1042 |
—— | ✓ | —— | ★ 能 |
- 两部分之间要有一个空行(第一部分末尾那行
0:0之后再空一行)。 E之后的所有内容一律不管 —— 包括管理员附注里说的那些非法字符。 一个字符一个字符读、读到E就break,这条就自动满足了。
- ★★ 把题面里每条规则圈出来,逐条问「样例的哪一行验了它」。 这道题三个错版,样例一个都挡不住 —— 因为那三条规则样例一次也没触发。
- ★ 「关键的一步」可以是「别抄第二遍」。 两种赛制只差一个数字, 写成一个带参数的函数,就没有「改了一处忘了另一处」这回事。
- ★★★ 对拍抓不到分两种:概率低(21 / 300),和结构上不可能(0 / 300)。 顺手生成器造 1~40 行,而那个 bug 要 2501 行 —— 多跑三万轮也没用。