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 统计单词数 —— 正解:读一整行,全转小写,逐词切分//// ★★ 这道题就是[第 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;}点「运行 ▶」看结果
2⚠ 错法一:第二行用 cin >> t 读 —— 它在回答另一道题
cin >> t 遇到空格就停 ⇒ 读进来的「文章」只剩第一个单词。
于是这一版恒等于在回答另一道题:
「给定单词是不是文章的第一个词?是就输出
1 0,不是就输出-1。」
⇒ ★★ 说清楚这一句之后,它的所有表现都是白送的推论 (第 14 章 P1332 立的那条规矩):
- 它放过的输入只有两种:答案本来就是
-1,或者「w 只出现一次、而且就在开头(下标 0)」; - 别的输入它一定错 ⇒ 下面那张表里它是四档 159 / 179 / 153 / 180, 而这四个数和上面那句话数出来的轮数一个不差。
⚠ 它最难受的地方是:程序不崩、格式完全正常、-Wall 一声不吭,
而官方样例一(2 0 → 它打 1 0)当场就打死它 —— 这一个它躲不掉。
// 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;}点「运行 ▶」看结果
3⚠ 错法二:cin >> w >> ws —— 正文那句提醒,量成了一个数
ws 的语义是「跳过所有空白」,而换行是空白、空格也是。
⇒ 文章开头有几个空格,读到的就短几个字符。
★ 而这不是钻牛角尖:题面第 2 行写的是「其中只可能包含字母和空格」, 输出格式那一节还专门补了一句「注意:空格占一个字母位」—— ⇒ 开头就是空格,是完全合法的输入,而且题面提前替你把这条路封死了。
★★ 这个 bug 只污染输出的一半(第 31 章 P2712 那条): 次数永远是对的,只有位置偏了,而偏移量恰好等于文章开头的空格数 —— 下面那张表里「位置差 ≡ 行首空格数」在两个专门档上是 179 / 179 和 180 / 180,一轮不差。
4⚠ 错法三:stringstream 切词,位置靠「词长 + 1」累加
while (ss >> x) {
if (x == w) { ... first = pos; }
pos += x.size() + 1; // ⚠ 这个 +1 假设了「词之间恰好一个空格」
}stringstream 帮你切词,代价是它不告诉你位置。于是所有人都会顺手写上面那一行 ——
而它把「词与词之间恰好一个空格」当成了公理。题面允许的是「只可能包含字母和空格」,
⇒ 连续两个空格、行首空格,都是合法输入。
★ 和上一个错法一样,它也只污染一半:次数永远对,位置偏。 ⚠ 而两个 bug 的触发条件部分重叠(行首空格两个都吃)—— 所以下面那张表要三档分开量,才看得出谁是谁的(第 46 章 P2114 那条的又一次)。
5⚠ 错法四:没找到时打的是 0 -1
题面的输出格式写着两句话:找到了输出两个整数;没找到直接输出一个整数 -1。
顺手写成「不管找没找到都打两个数」,在「一次都没出现」的那些测试点上全 WA。
⇒ 被抓的轮数 ≡ 正解输出 -1 的轮数(四档 123 / 121 / 128 / 120,一个不差)。
★ 而官方样例二正好就是这种输入 ⇒ 这个错法一交样例就死。
6★★★ 对拍:四个错法 × 四个档位,十六格「触发 ≡ 抓获」一个不差
// 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 |
-
★★★ 十六格全部是「触发 ≡ 抓获」,而四条触发条件长得完全不一样:
错法 它的触发条件(一句话) cin >> t答案不是 -1,而且不是「w 只出现一次且就在下标 0」>> ws文章开头有空格,而且答案不是 -1累加位置 第一次命中之前出现过多余的空格(行首空格 或 连续两个空格),且答案不是 -1打 0 -1答案正好是 -1⇒ ★★ 四条都能写成一句精确的话,所以 ≡ 是必然的 (第 44 章那条:它是不是运气,取决于你能不能把「它算了什么」写成式子)。
-
★★★ 顺手写的那一档(档 0)把两个错法一起打成了精确的 0 —— 而那两个 0 不是概率低,是结构性的:谁会主动在文章开头放一个空格、在两个词之间放两个空格? ⇒ 而题面明写着文章「只可能包含字母和空格」,这两种形状全都合法。 ⇒ ★★ 第 22 章 P1020 那条规矩在这儿要再推一步: 生成器该照抄的不只是题面的规模和比值,还有题面允许的那些形状。
-
★ 两个「只污染一半」的 bug 各自的一半都验过了:
>> ws版和累加版的次数在 1200 轮里和正解完全相同, 而>> ws版的位置差逐轮等于「文章开头的空格数」。
7★★ 读入那笔账:这道题的读入关是「读对」,不是「读快」
// 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 倍 |
⚠ 内存那笔账更省事:w 和 t 加起来约 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 章 P1469(ru_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 | 样例二就死 |
⇒ ★★★ 一句话带走:这道题的四个坑一个都不在算法里 —— 两个在「有没有把整行读进来」,一个在「位置从哪儿数」,一个在「输出格式」。 ⇒ 而它们共用一句判据:把题面那两行「输入格式」逐字读一遍, 问问自己「这句话允许的输入,我的程序真的能读对吗」。