题单 · 习题解析

洛谷 P1042 乒乓球

★★ 三个错版,样例一个都挡不住;而对拍抓不到也分两种:概率低(21/300)和结构上不可能(0/300)

原题:洛谷 P1042出自 第 5 章 枚举与模拟 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

国际乒联现在主席沙拉拉自从上任以来就立志于推行一系列改革,以推动乒乓球运动在全球的普及。 其中 11 分制改革引起了很大的争议,有一部分球员因为无法适应新规则只能选择退役。 华华就是其中一位,他退役之后走上了乒乓球研究工作, 意图弄明白 11 分制和 21 分制对选手的不同影响。

题目描述

华华通过以下方式进行分析,首先将比赛每个球的胜负列成一张表, 然后分别计算在 11 分制和 21 分制下,双方的比赛结果(截至记录末尾)。

比如现在有这么一份记录(其中 W 表示华华获得一分,L 表示华华对手获得一分):

WWWWWWWWWWWWWWWWWWWWWWLW

11 分制下,此时比赛的结果是华华第一局 110 获胜,第二局 110 获胜, 正在进行第三局,当前比分 11。而在 21 分制下,此时比赛结果是华华第一局 210 获胜,正在进行第二局,比分 21如果一局比赛刚开始,则此时比分为 00。 直到分差大于或者等于 2,才一局结束。

注意:当一局比赛结束后,下一局立刻开始。

你的程序就是要对于一系列比赛信息的输入(WL 形式),输出正确的结果。

输入格式:每个输入文件包含若干行字符串,字符串由大写的 WLE 组成。 其中 E 表示比赛信息结束,程序应该忽略 E 之后的所有内容。

输出格式:输出由两部分组成,每部分有若干行,每一行对应一局比赛的比分(按比赛信息输入顺序)。 其中第一部分是 11 分制下的结果,第二部分是 21 分制下的结果,两部分之间由一个空行分隔。

数据范围:每行至多 25 个字母,最多有 2500 行。

【管理员附注】 本题为非常早期的试题,在测试点中出现了如下两个问题。在洛谷上这两个测试点的疏漏被保留:

  1. 有一个测试点实际上有 2501 行数据。
  2. 有一个测试点的输入数据出现了非 WLE 的字符,不符合输入格式的要求。 不过这些字符只出现在 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 分谁赢(漏了半句话)

p1042Naive.cpp第 ① 版:忘了分差 >= 2
样例一字不差。⚠ 把输入换成 25 个 W 和 L 交替(WLWLWL…)再看 —— 那才是它现原形的地方。
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面那半句是:「直到分差大于或者等于 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);                  // ← 正解
p1042NoTail.cpp第 ② 版:0:0 就不打了
这一组输入 11 个 W:11 分制下正好打完一局,正解会再打一行 0:0,它没有。
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 「一局刚打完」和「一局刚开始」是同一个时刻

题面写了两句话,得合起来读:

  • 「当一局比赛结束后,下一局立刻开始
  • 「如果一局比赛刚开始,则此时比分为 00

⇒ 记录正好在一局打完时结束时,新的一局已经开始了,它的比分是 0:0,要打出来。 直觉上「0:0 有什么好输出的」,可它是题目明说要的一行。

★ 顺带一句:11 分制和 21 分制各自判断这件事。 一份记录可能是「11 分制正好打完、21 分制打了一半」,两部分的末尾行互不相干。

4★ 正解:两种赛制只差一个数字 —— 那就只写一遍

p1042.cpp正解:一个函数,跑两遍
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 这道题的「关键的一步」不是算法,是别抄第二遍

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 行数据。」

p1042Limit.cpp第 ③ 版:只读 2500 行
⚠ 要记的不是「2501」这个数

这道题是 2003 年的老题,数据本身有疏漏,而洛谷选择保留它。 下次遇到的可能是别的数、别的题,所以要带走的是那个习惯:

读到没有为止(while (cin >> …)),别拿题面给的上界当循环边界。 数组开大一点(乘 2、加 10)也是同一条:上界写错了,你就跟着错。

6★★★ 对拍:三个错版,三种「有多难抓」

p1042Gen.cpp生成器:三个档位
参数是「种子 档位」。档位 1 造的记录,正好在一局打完时结束。
// 数据生成器(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 之后的所有内容一律不管 —— 包括管理员附注里说的那些非法字符。 一个字符一个字符读、读到 Ebreak,这条就自动满足了。
这一页记住三句话
  1. ★★ 把题面里每条规则圈出来,逐条问「样例的哪一行验了它」。 这道题三个错版,样例一个都挡不住 —— 因为那三条规则样例一次也没触发。
  2. 「关键的一步」可以是「别抄第二遍」。 两种赛制只差一个数字, 写成一个带参数的函数,就没有「改了一处忘了另一处」这回事。
  3. ★★★ 对拍抓不到分两种:概率低(21 / 300),和结构上不可能(0 / 300)。 顺手生成器造 1~40 行,而那个 bug 要 2501 行 —— 多跑三万轮也没用。