题单 · 习题解析

洛谷 P1308 [NOIP 2011 普及组] 统计单词数

★★★ 它就是[第 47 章正文](/ch/47-string-basics/)那道题的**原题**,题面几乎一字不差 ⇒ 这一页不再讲一遍「切词 vs 补空格」,而是去量正文只提了两句、没有称过重量的那一关:**读入**;★★★ 四个错法**没有一个在算法里** —— `cin >> t` 只读到第一个空格(它恒等于在回答另一道题:「w 是不是文章的第一个词」)/ `cin >> w >> ws` 连**文章开头的空格**一起吃掉(正文第 2 步那句没有数字的提醒,这里量成了 179 / 300)/ `stringstream` 切词后 `pos += x.size() + 1` 把「词间恰好一个空格」当成了公理 / 没找到时打 `0 -1`;★★★ 十六格「触发 ≡ 抓获」**一个不差**,而四条触发条件形状完全不同;★★★ 顺手写的那一档把中间两个错法一起打成**结构性的精确的 0** —— 谁会主动在文章开头放空格、在两个词之间放两个空格?可题面写的是「只可能包含字母和空格」⇒ **生成器该照抄的不只是题面的规模和比值,还有题面允许的那些形状**;★★ 读入那笔账的结论是「不用管」:四种读法差 **14.7 倍**(默认 `cin` 10.6 ms / 关同步 0.72 ms / `scanf` 2.0 / `fread` 1.3),可顶格 10⁶ 个字符下最慢的也只用 10.6 毫秒 ⇒ **余量 94 倍** —— 倍数跨题几乎不变,分数线在绝对时间上;★ 官方两组样例各打死一个错法,而剩下两个**两组都碰不到**,因为它们要的形状(行首空格 / 连续空格)样例里一次都没出现

原题:洛谷 P1308出自 第 47 章 字符串基础:读进来、切开、比对 的题单题面本地存档:2026-09-07
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

一般的文本编辑器都有查找单词的功能,该功能可以快速定位特定单词在文章中的位置, 有的还能统计出特定单词在文章中出现的次数。

现在,请你编程实现这一功能,具体要求是:给定一个单词,请你输出它在给定的文章中出现的次数和 第一次出现的位置。注意:匹配单词时,不区分大小写,但要求完全匹配,即给定单词必须与文章中的 某一独立单词在不区分大小写的情况下完全相同(参见样例 1),如果给定单词仅是文章中某一单词的 一部分则不算匹配(参见样例 2)。

输入格式

共 2 行。

第 1 行为一个字符串,其中只含字母,表示给定单词;

第 2 行为一个字符串,其中只可能包含字母和空格,表示给定的文章。

输出格式

一行,如果在文章中找到给定单词则输出两个整数,两个整数之间用一个空格隔开,分别是单词在文章中 出现的次数和第一次出现的位置(即在文章中第一次出现时,单词首字母在文章中的位置,位置从 0 开始); 如果单词在文章中没有出现,则直接输出一个整数 -1

注意:空格占一个字母位。

数据范围

1 ≤ 第一行单词长度 ≤ 10。

1 ≤ 文章长度 ≤ 10⁶。

noip2011 普及组第 2 题。时限 1 秒,内存 128000 KB(125 MB)。

输入输出样例

输入

To
to be or not to be is a question

输出

2 0

to 出现两次,第一次的首字母在下标 0。⚠ 单词写的是 To、文章里是 to —— 不区分大小写

输入

to
Did the Ottoman Empire lose its power at that time

输出

-1

to 明明在 Ottoman 里出现过,可那不是一个完整的单词 ⇒ 一次都不算,输出一个 -1

1★★ 这一页不再讲一遍「切词」—— 它去量正文没称过重量的那一关

★★★ 「正文用过这道题」不等于「解析页没得写」

这道题就是第 47 章正文那道题的原题,而且题面几乎一字不差 (不像第 46 章 P1469 那次 —— 那道题正文和真题的数据范围差了 32 倍)。 ⇒ 正文第 4、5、7、10 步已经把「切词 vs 补空格」「子串不是整词」「stringstream 的两个代价」 讲透了,这一页再讲一遍没有意义

★ 所以这一页换一个角度:读入。正文第 2 步只用一张表提了三种写法, 其中第 ③ 行后面挂着一句没有数字的提醒(「>> ws 会把正文开头的空格也吃掉」), 第 7 步也挂着一句(「一旦问位置,stringstream 那一步很容易差一格」)。 ⇒ 这一页把那两句提醒各量成了一个数,外加两个只跟输入输出有关的错法。

⚠⚠ 而这四个错法有一个共同点,它才是这一页真正的主线: 它们全都不是「算法写错了」,是「读进来的东西和题面说的不是一回事」。

p1308.cpp★ 正解:读一整行 → 全转小写 → 逐词切分
// P1308 统计单词数 —— 正解:读一整行,全转小写,逐词切分
//
// ★★ 这道题就是[第 47 章正文](/ch/47-string-basics/)那道题的原题,题面几乎一字不差。
// ⇒ 所以这一页**不再讲一遍「切词 vs 补空格」**(正文第 4、5、7 步已经讲透了),
// 它去量正文只提了一句、没有称过重量的那一关:**读入**。
//
// ⚠ 这一份里和读入有关的有三处,每一处都对应这一页的一个错法:
// ① 第二行必须 `getline` 读**一整行** —— `cin >> t` 只会读到第一个空格(见 p1308Word.cpp);
// ② `cin >> w` 之后要先把那个残留的换行吃掉,而**不能用 `>> ws`** ——
// `ws` 会连**文章开头的空格**一起吃掉,而题面说文章「只可能包含字母和空格」,
// 开头就是空格是合法输入,位置会整体偏移(见 p1308Ws.cpp);
// ③ 位置要从**原串的下标**来,别拿切出来的词长累加(见 p1308Stream.cpp)。
#include <bits/stdc++.h>
using namespace std;
int main() {
string w, t;
if (!(cin >> w)) return 0;
getline(cin, t); // ★ 先吃掉第一行剩下的换行符(正文第 2 步那个坑)
if (!getline(cin, t)) t = ""; // ⚠ 文章可能是空行 —— 这是合法输入
for (char& c : w) c = (char)tolower((unsigned char)c);
for (char& c : t) c = (char)tolower((unsigned char)c);
long long cnt = 0;
long long first = -1;
size_t i = 0, n = t.size();
while (i < n) {
while (i < n && t[i] == ' ') i++; // 跳过空格 -> 停在一个词的开头
if (i >= n) break;
size_t j = i;
while (j < n && t[j] != ' ') j++; // [i, j) 就是一个完整的词
if (j - i == w.size() && t.compare(i, j - i, w) == 0) {
cnt++;
if (first < 0) first = (long long)i; // ★ 位置直接就是原串的下标
}
i = j;
}
if (cnt == 0) printf("-1\n");
else printf("%lld %lld\n", cnt, first);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1308Pad.cpp★ 参照物:补空格版(对「单词边界」的理解完全不同)

2⚠ 错法一:第二行用 cin >> t 读 —— 它在回答另一道题

★★ 说清楚它算了什么,比说它错了有用得多

cin >> t 遇到空格就停 ⇒ 读进来的「文章」只剩第一个单词。 于是这一版恒等于在回答另一道题:

「给定单词是不是文章的第一个词?是就输出 1 0,不是就输出 -1。」

⇒ ★★ 说清楚这一句之后,它的所有表现都是白送的推论 (第 14 章 P1332 立的那条规矩):

  • 放过的输入只有两种:答案本来就是 -1,或者「w 只出现一次、而且就在开头(下标 0)」;
  • 别的输入它一定错 ⇒ 下面那张表里它是四档 159 / 179 / 153 / 180, 而这四个数和上面那句话数出来的轮数一个不差

⚠ 它最难受的地方是:程序不崩、格式完全正常、-Wall 一声不吭, 而官方样例一(2 0 → 它打 1 0)当场就打死它 —— 这一个它躲不掉。

p1308Word.cpp✗ 错法一:cin >> t 只读到第一个空格
// P1308 ✗ 错法一:第二行用 `cin >> t` 读
//
// ⚠ 这是这道题上最常见的第一版 —— 它一个字都不错,只是**读少了**:
// `cin >> t` 遇到空格就停,于是「文章」只剩下第一个单词。
// ⇒ ★ 它的触发条件是一句话:**文章里不止一个词**(也就是几乎所有输入)。
// 而 `-Wall` 不会响、程序不会崩、格式完全正常 —— 它只是**在回答另一道题**:
// 「w 等不等于文章的第一个词」。
#include <bits/stdc++.h>
using namespace std;
int main() {
string w, t;
if (!(cin >> w)) return 0;
if (!(cin >> t)) t = ""; // ⚠ 只读到第一个空格
for (char& c : w) c = (char)tolower((unsigned char)c);
for (char& c : t) c = (char)tolower((unsigned char)c);
long long cnt = 0, first = -1;
size_t i = 0, n = t.size();
while (i < n) {
while (i < n && t[i] == ' ') i++;
if (i >= n) break;
size_t j = i;
while (j < n && t[j] != ' ') j++;
if (j - i == w.size() && t.compare(i, j - i, w) == 0) { cnt++; if (first < 0) first = (long long)i; }
i = j;
}
if (cnt == 0) printf("-1\n");
else printf("%lld %lld\n", cnt, first);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠ 错法二:cin >> w >> ws —— 正文那句提醒,量成了一个数

★★★ ws 吃掉的不只是换行,还有文章开头的空格

ws 的语义是「跳过所有空白」,而换行是空白、空格也是。 ⇒ 文章开头有几个空格,读到的就短几个字符。

★ 而这不是钻牛角尖:题面第 2 行写的是「其中只可能包含字母和空格」, 输出格式那一节还专门补了一句「注意:空格占一个字母位」—— ⇒ 开头就是空格,是完全合法的输入,而且题面提前替你把这条路封死了。

★★ 这个 bug 只污染输出的一半(第 31 章 P2712 那条): 次数永远是对的,只有位置偏了,而偏移量恰好等于文章开头的空格数 —— 下面那张表里「位置差 ≡ 行首空格数」在两个专门档上是 179 / 179180 / 180,一轮不差。

p1308Ws.cpp✗ 错法二:>> ws 把行首空格也吃掉了

4⚠ 错法三:stringstream 切词,位置靠「词长 + 1」累加

★★ 正文第 7 步那句「很容易差一格」,差在哪儿
   while (ss >> x) {
       if (x == w) { ... first = pos; }
       pos += x.size() + 1;          // ⚠ 这个 +1 假设了「词之间恰好一个空格」
   }

stringstream 帮你切词,代价是它不告诉你位置。于是所有人都会顺手写上面那一行 —— 而它把「词与词之间恰好一个空格」当成了公理。题面允许的是「只可能包含字母和空格」, ⇒ 连续两个空格、行首空格,都是合法输入。

★ 和上一个错法一样,它也只污染一半:次数永远对,位置偏。 ⚠ 而两个 bug 的触发条件部分重叠(行首空格两个都吃)—— 所以下面那张表要三档分开量,才看得出谁是谁的(第 46 章 P2114 那条的又一次)。

p1308Stream.cpp✗ 错法三:位置靠词长累加

5⚠ 错法四:没找到时打的是 0 -1

★ 和算法一点关系都没有,而它的触发条件是一句等式

题面的输出格式写着两句话:找到了输出两个整数;没找到直接输出一个整数 -1。 顺手写成「不管找没找到都打两个数」,在「一次都没出现」的那些测试点上全 WA。

被抓的轮数 ≡ 正解输出 -1 的轮数(四档 123 / 121 / 128 / 120,一个不差)。 ★ 而官方样例二正好就是这种输入 ⇒ 这个错法一交样例就死。

p1308Neg.cpp✗ 错法四:打出 0 -1

6★★★ 对拍:四个错法 × 四个档位,十六格「触发 ≡ 抓获」一个不差

p1308Gen.cpp★ 生成器:两个旋钮 —— 行首空格 / 连续空格
⚠ 它自己不判对错,只造数据;下面那张表是 check:viz 里 4 档 × 300 轮跑出来的。
// P1308 的生成器:./p1308Gen 种子 [档位]
//
// ★ 动笔之前先写清楚「每个错法靠什么现形」([第 46 章第 13 步](/ch/46-bitwise/)那张清单):
// · p1308Word(`cin >> t`) → **文章不止一个词**(几乎所有输入,一抓一个准);
// · p1308Ws(`>> ws`) → **文章开头有空格**,而且答案不是 -1;
// · p1308Stream(位置靠词长累加) → **第一次命中之前出现过多余的空格**(行首空格 或 连续空格);
// · p1308Neg(没找到打 `0 -1`) → **答案正好是 -1**。
//
// ⚠⚠ 关键在于:后三条要的那些输入,**顺手写的生成器一个都不会造** ——
// 谁会主动在文章开头放空格、在词之间放两个空格?可题面只说文章
// 「只可能包含字母和空格」,这三种全是**合法输入**。
// ⇒ 这正是[第 20 章 P1020](/sol/p1020/)那条「生成器该照抄题面」的另一面:
// **照抄的不只是规模,还有题面允许的那些形状。**
//
// 档位:
// 0 ★ 顺手写的:3~12 个词、词间恰好一个空格、开头不留空格
// 1 ⚠ 开头留 1~3 个空格(p1308Ws 的专门档)
// 2 ⚠ 词间 1~3 个空格(p1308Stream 的专门档)
// 3 ★ 最终档:两个旋钮一起开
// 9 ★ 顶格:文章正好 10⁶ 个字符(题面上限)—— 给 p1308Read.cpp 量读入用
//
// ⚠ rng() 一律先落到具名变量再传参([第 24 章 P1776](/sol/p1776/) 那一跤)。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int mode = argc > 2 ? atoi(argv[2]) : 0;
mt19937 rng(seed * 1000003u + 20260907u);
const bool leadSpace = (mode == 1 || mode == 3);
const bool multiSpace = (mode == 2 || mode == 3);
const bool huge = (mode == 9);
/* 词表:几个短词,其中一个是要找的 w —— 短词表才有足够的命中率 */
const char* pool[] = {"to", "the", "be", "or", "not", "is", "a", "an", "at"};
const int POOL = 9;
unsigned r0 = rng() % (unsigned)POOL;
string w = pool[r0];
unsigned r1 = rng() % 10u;
int words = 3 + (int)r1;
if (huge) words = 1 << 30; // ★ 顶格档:写满 10⁶ 个字符为止
string t;
if (leadSpace) { unsigned r = rng() % 3u; t.append((size_t)(1 + r), ' '); }
for (int i = 0; i < words; i++) {
if (huge && t.size() >= 999990) break; // ★ 顶格档:留出最后一个词的余量
if (i) {
int gap = 1;
if (multiSpace) { unsigned r = rng() % 3u; gap = 1 + (int)r; }
t.append((size_t)gap, ' ');
}
unsigned r = rng() % (unsigned)POOL;
string x = pool[r];
/* 大小写随机翻转 —— 题面说「不区分大小写」*/
for (char& c : x) { unsigned f = rng() % 2u; if (f) c = (char)toupper((unsigned char)c); }
t += x;
}
if (huge) while (t.size() < 1000000) t.push_back('x'); // 补到正好 10⁶
unsigned rw = rng() % 2u;
if (rw) for (char& c : w) c = (char)toupper((unsigned char)c);
printf("%s\n%s\n", w.c_str(), t.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

四档 × 300 轮(正解 vs 补空格版:1200 轮 0 组不一致):

档位 cin >> t >> ws ✗ 累加位置 ✗ 打 0 -1
0 ★ 顺手写的(词间恰好一个空格、开头不留空格) 159 0 0 123
1 ⚠ 行首留 1~3 个空格 179 179 179 121
2 ⚠ 词间 1~3 个空格 153 0 121 128
3 ★ 两个旋钮一起开 180 180 180 120
★★★ 三条读得出来的结论
  1. ★★★ 十六格全部是「触发 ≡ 抓获」,而四条触发条件长得完全不一样

    错法 它的触发条件(一句话)
    cin >> t 答案不是 -1而且不是「w 只出现一次且就在下标 0」
    >> ws 文章开头有空格,而且答案不是 -1
    累加位置 第一次命中之前出现过多余的空格(行首空格 或 连续两个空格),且答案不是 -1
    0 -1 答案正好是 -1

    ⇒ ★★ 四条都能写成一句精确的话,所以 ≡ 是必然的 (第 44 章那条:它是不是运气,取决于你能不能把「它算了什么」写成式子)。

  2. ★★★ 顺手写的那一档(档 0)把两个错法一起打成了精确的 0 —— 而那两个 0 不是概率低,是结构性的:谁会主动在文章开头放一个空格、在两个词之间放两个空格? ⇒ 而题面明写着文章「只可能包含字母和空格」,这两种形状全都合法。 ⇒ ★★ 第 22 章 P1020 那条规矩在这儿要再推一步: 生成器该照抄的不只是题面的规模和比值,还有题面允许的那些形状。

  3. 两个「只污染一半」的 bug 各自的一半都验过了>> ws 版和累加版的次数在 1200 轮里和正解完全相同, 而 >> ws 版的位置差逐轮等于「文章开头的空格数」。

7★★ 读入那笔账:这道题的读入关是「读对」,不是「读快」

p1308Read.cpp★ 四种读法读同一份顶格输入,各报各的毫秒
⚠ 它把读入和算答案分开计时 —— 不分开的话,读入那一截会被算答案那 3.4 毫秒盖住(第 34 章 P3366 那条)。
// P1308 —— 四种读法读同一份输入,各自报「读进来花了多少毫秒」。
// ./p1308Read sync [文件] 默认的 cin + getline(什么都不关)
// ./p1308Read nosync [文件] ios::sync_with_stdio(false) + getline
// ./p1308Read scanf [文件] scanf("%s") + scanf("%[^\n]")
// ./p1308Read fread [文件] 一次 fread 把整份输入吞进来,自己切两行
//
// ⚠⚠⚠ 第二个参数不是可有可无的(2026-09-07 在 check:viz 里当场撞出来的):
// 给了文件就 `freopen` 到 stdin 上 —— **评测机做的正是这件事**(把 stdin 重定向到一个文件)。
// 不给文件时读的是**管道**,而管道那一头是谁、写得多快,**会被一起量进来**:
// 同一个二进制、同一份数据,从文件读 0.7 毫秒,被 node 用管道喂是 3 毫秒以上
// ——⇒ 关同步那一版快到「它在等喂数据的人」,量到的于是不再是它自己的速度。
// ⇒ ★★ 这和[第 46 章 P1469](/sol/p1469/) 那条「量内存要量差值,因为起步那一截取决于谁 spawn 它」
// 是同一件事的另一半:**量什么都要先问一句「这个数里有没有别人的份」。**
//
// ★ 为什么要单独量这一关:题面的文章长度是 **10⁶**,
// 而[第 6 章 P2367](/sol/p2367/) / [第 38 章 P1972](/sol/p1972/) 那两页立过一条规矩 ——
// **四种读法的倍数跨题几乎不变,变的是绝对时间,而分数线在绝对时间上。**
// ⇒ 所以这道题该不该写读入优化,是一道要**乘一遍**才能回答的题,不能背。
//
// ⚠ 输出里也带上算出来的答案:四种读法**必须给出同一个答案**,
// 否则量到的就是「读丢了一截」而不是「读得快」([第 12 章 P1923](/sol/p1923/) 那个 33.5 MB 的桶)。
#include <bits/stdc++.h>
using namespace std;
static string answer(string w, string t) {
for (char& c : w) c = (char)tolower((unsigned char)c);
for (char& c : t) c = (char)tolower((unsigned char)c);
long long cnt = 0, first = -1;
size_t i = 0, n = t.size();
while (i < n) {
while (i < n && t[i] == ' ') i++;
if (i >= n) break;
size_t j = i;
while (j < n && t[j] != ' ') j++;
if (j - i == w.size() && t.compare(i, j - i, w) == 0) { cnt++; if (first < 0) first = (long long)i; }
i = j;
}
char buf[64];
if (cnt == 0) snprintf(buf, sizeof(buf), "-1");
else snprintf(buf, sizeof(buf), "%lld %lld", cnt, first);
return string(buf);
}
static vector<char> big; // scanf / fread 两种模式要的落脚处
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "sync";
if (argc > 2 && !freopen(argv[2], "r", stdin)) { fprintf(stderr, "open failed\n"); return 1; }
string w, t;
auto t0 = chrono::steady_clock::now();
if (mode == "nosync") {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> w;
getline(cin, t);
if (!getline(cin, t)) t = "";
} else if (mode == "scanf") {
big.assign(1 << 21, 0);
if (scanf("%s", big.data()) == 1) w = big.data();
int c = getchar();
while (c != EOF && c != '\n') c = getchar();
big[0] = 0;
if (scanf("%1048576[^\n]", big.data()) == 1) t = big.data(); else t = "";
} else if (mode == "fread") {
big.clear();
char chunk[1 << 16];
size_t got;
while ((got = fread(chunk, 1, sizeof(chunk), stdin)) > 0) big.insert(big.end(), chunk, chunk + got);
size_t p = 0, n = big.size();
while (p < n && big[p] != '\n') p++;
w.assign(big.begin(), big.begin() + (long)p);
size_t q = ++p;
while (q < n && big[q] != '\n') q++;
if (p <= n) t.assign(big.begin() + (long)p, big.begin() + (long)min(q, n));
} else {
cin >> w; // ★ 默认:sync_with_stdio 开着
getline(cin, t);
if (!getline(cin, t)) t = "";
}
auto t1 = chrono::steady_clock::now();
double readMs = chrono::duration<double, milli>(t1 - t0).count();
auto t2 = chrono::steady_clock::now();
string ans = answer(w, t);
auto t3 = chrono::steady_clock::now();
double calcMs = chrono::duration<double, milli>(t3 - t2).count();
printf("%s,%.2f,%.2f,%zu,%s\n", mode.c_str(), readMs, calcMs, t.size(), ans.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(A 机:WSL2 6.18 / 8 核 / 8 GB,2026-09-07,独占;顶格输入:文章正好 10⁶ 个字符, 约 3.5 万个单词;每种读法跑 3 次取中位数):

读法 读进来 算答案 合计 相对最慢
默认 cin + getline(什么都不关) 10.6 ms 3.4 ms 14.0 ms 1.0×
ios::sync_with_stdio(false) + getline 0.72 ms 3.4 ms 4.1 ms 14.7 倍
scanf("%[^\n]") 2.0 ms 3.4 ms 5.4 ms 5.3 倍
一次 fread 吞进来 1.3 ms 3.4 ms 4.7 ms 7.9 倍
★★★ 倍数很大,而结论是「这道题不用管」

四种读法之间差 14.7 倍 —— 和第 6 章 P2367第 41 章 P3383 那几页量到的倍数是同一个量级。可这道题最慢的那一种也只要 10.6 毫秒,而时限是 1 秒。

⇒ ★★★ 这正是那条规矩的又一次现场: 四种读法的倍数跨题几乎不变,变的是绝对时间,而分数线在绝对时间上。

要读多少 时限 分数线在哪
第 6 章 P2367 2 × 10⁷ 个整数 1 秒 scanf 都不够(要快读)
第 38 章 P1972 约 20 MB 2 秒 默认 cin 够,余量只剩 2.9 倍
本题 10⁶ 个字符(约 1 MB) 1 秒 四种全够,余量 94 倍

⚠ 内存那笔账更省事:wt 加起来约 1 MB,限制 125 MB ⇒ 一句除法就问完了。

⇒ ★★ 所以这道题的读入关整个长在「读对」那一侧cin >> t 少读了一大截、>> ws 少读了开头几个空格 —— 两个都不是慢,是

⚠⚠ 量这张表的时候踩了一个:从管道读,喂数据的那一头也会被量进去

上面那张表是从文件读量的(freopen)—— 评测机做的正是这件事:把 stdin 重定向到一个文件。

★ 第一版的断言图省事,直接让 check:viz管道把那 1 MB 喂进去。结果:

从文件读 从管道读(node 一边写一边被读) 多出来
默认 cin 10.8 ms 11.6 ms +0.8 ms(+8%
★ 关同步 0.94 ms 1.45 ms +0.5 ms(★ +54%
scanf 2.14 ms 2.60 ms +0.5 ms(+21%)
fread 1.63 ms 1.89 ms +0.3 ms(+16%)

(这一趟是在 check:viz 里量的,每格 3 次取中位数。⚠ 那一列多出来的是亚毫秒的量, 换一趟能差两成 ⇒ 断言里只钉「四种读法多出来的都不到 2.5 毫秒」和「结论本身不受影响」这两件事, 百分比只当量级看 —— 第 38 章 P1177 那条:亚毫秒的秒表不能只跑一次。)

多出来的那一截四种读法几乎一样多(都不到 1 毫秒),因为它根本不是程序的属性 —— 它是喂数据那一头的速度。可它的占比从 8% 一路走到 50% 上下: 关同步那一版快到「它在等写数据的人」,量到的就不再是它自己了。

⇒ ★★★ 一个加性开销会把小数字整个吃掉,却在大数字上完全隐形 —— 和第 34 章 P3366(不减掉读入那 19 毫秒会得出相反结论)、 第 46 章 P1469ru_maxrss 里「进程起步」那一截取决于谁 spawn 它) 是同一件事的第三次:⇒ 量什么都要先问一句「这个数里有没有别人的份」。

⚠ 代价很具体:第一版的断言写的是「关同步快 14.7 倍」,在管道上当场变成 8.0 倍, 正好卡在门槛上翻红。

8★ 官方两组样例:各打死一个,另外两个它们都碰不到

★★ 「样例是一测就死的过滤器」这一次的形状
cin >> t >> ws ✗ 累加位置 ✗ 打 0 -1
样例一(2 0 (打 1 0 放过 放过 放过
样例二(-1 放过 放过 放过 (打 0 -1

★ 两组样例各打死一个,而剩下那两个两组都放过 —— 原因说得出来: 它俩要的输入形状是「行首空格」和「连续两个空格」, 而官方那两行文章都是干干净净的「一个词一个空格」。

⇒ ★★★ 这是「这组样例在结构上问不出这个问题」的又一次, 而这一次「结构」两个字特别具体:它就是题面允许、样例里却一次都没出现的那两种空格。 ⇒ 而顺手写的生成器和官方样例恰好犯同一个毛病(上一步档 0 那两个精确的 0)—— ⚠ 样例和对拍一起漏掉的那一格,正是最难自己想到的那一格。

9★ 哪一版就已经能过了

★ 第一版写对了就能过 —— 这道题没有第二关
版本 结果 说明
p1308.cpp(切词) AC 顶格 14 毫秒 / 时限 1 秒,余量 71 倍
p1308Pad.cpp(补空格) AC 1200 轮和切词版逐字节相同;选哪条只看你顺手
cin >> t WA 样例一就死
>> ws WA ⚠ 样例全过,位置偏
✗ 累加位置 WA ⚠ 样例全过,位置偏
✗ 打 0 -1 WA 样例二就死

⇒ ★★★ 一句话带走:这道题的四个坑一个都不在算法里 —— 两个在「有没有把整行读进来」,一个在「位置从哪儿数」,一个在「输出格式」。 ⇒ 而它们共用一句判据:把题面那两行「输入格式」逐字读一遍, 问问自己「这句话允许的输入,我的程序真的能读对吗」。