题单 · 习题解析

洛谷 P5015 [NOIP 2018 普及组] 标题统计

★★★ 这一页一个生成器都没写 —— 题面 100% 档的输入空间是 Σ 63^L = **1 008 443 583** 个(10.08 亿),而**答案只取决于每个字符的类别**(空格 / 字母 / 数字)⇒ **3 类 × 长度 1~5 = 363 个代表输入**就覆盖了全部;★★★ 而「只取决于类别」这句话是**验过**的(真实 63 个字符枚举长度 1~3 的全部 254 079 个输入,逐个和代表比,0 个不同)—— [第 19 章 P2240](/sol/p2240/) 那条规矩换了个位置用:**先证明「压缩」这一步没丢东西,再去枚举压缩后的空间**;★★ 三个错法在 363 个代表上是 160 / **363(一个不落)** / 301,「从哪个长度开始错」是三个精确的整数 **3 / 1 / 1**;★★ 题面 40% / 80% / 100% 三档的 `|s|` 上界**一字不差都是 5**(原题就这么写的)⇒ 那三档分的**不是规模,是字符集** —— 而「只数数字字符」正是照着第一档写出来的,值 40 分;★ 顺带把「会不会超时」这一维消掉:`|s|` 放大 20 万倍到 10⁶,数一遍只要 **0.45 毫秒**;★★★ 官方两组样例把三个错法**全部打死**,而同一张题单里的 [P1308](/sol/p1308/) 是另一个极端(两组各只打死一个,剩下两个都碰不到)⇒ **「样例挡不挡得住」的主语是那几组样例的结构**

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

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

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

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

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

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

题目背景

NOIP2018 普及组 T1。

题目描述

凯凯刚写了一篇美妙的作文,请问这篇作文的标题中有多少个字符? 注意:标题中可能包含大、小写英文字母、数字字符、空格和换行符。统计标题字符数时,空格和换行符不计算在内。

输入格式

输入文件只有一行,一个字符串 s

输出格式

输出文件只有一行,包含一个整数,即作文标题的字符数(不含空格和换行符)。

数据规模与约定

规定 |s| 表示字符串 s 的长度(即字符串中的字符和空格数)。

对于 40% 的数据,1 ≤ |s| ≤ 5,保证输入为数字字符及行末换行符。

对于 80% 的数据,1 ≤ |s| ≤ 5,输入只可能包含大、小写英文字母、数字字符及行末换行符。

对于 100% 的数据,1 ≤ |s| ≤ 5,输入可能包含大、小写英文字母、数字字符、空格和行末换行符。

⚠⚠ 那三行不是转录错,原题就是这么写的 —— 三档的 |s| 上界一字不差都是 5。 第 ④ 步会说这件事到底意味着什么。

时限 1 秒,内存 262144 KB(256 MB)。

输入输出样例

输入

234 

输出

3

标题里共有 3 个字符,这 3 个字符都是数字字符。⚠ 行末那个空格不算。

输入

Ca 45 

输出

4

★ 标题里共有 5 个字符:1 个大写字母、1 个小写字母、2 个数字,还有 1 个空格。 空格不计入结果 ⇒ 有效字符数是 4。

1★ 一句话:这道题的全部内容就是「把那一行读对」

★★ 算法这一栏是空的

要做的事只有一句:读一行,数出不是空格的字符有几个。没有算法、没有数据结构、没有边界分析。 ⇒ 所以这一页从头到尾只在问一个问题:你有没有把那一行完整地读进来。

★ 而三个最常见的错法,恰好对应读入的三个动作:

错法 它把哪个动作做错了
cin >> s :遇到空格就停了 ⇒ 标题只剩第一段
逐字符读到 EOF,只排掉空格 :题面那句话是「空格和换行符」两样,漏了一样
只数数字字符 读题:照着「40% 的数据」那一档写的
p5015.cpp★ 正解:getline 一整行,数不是空格的
// P5015 标题统计 —— 正解:读一整行,数「不是空格」的字符
//
// ★ 这道题的全部内容就是一句话:**标题在一行里,而这一行含空格** ⇒ 只能 `getline`。
// ⚠ 三个容易漏的角,全在题面里写着:
// ① 「空格和换行符不计算在内」—— 换行不是 `getline` 读出来的东西(它把换行吃掉了),
// 所以只要排掉空格;⚠ 换成逐字符读到 EOF 的写法就必须自己排掉换行(见 p5015Nl.cpp);
// ② 标题**可以以空格开头**,也可以整行都是空格 ⇒ 答案 0 是合法输出;
// ③ 题面三档数据范围**一字不差都写着 `1 ≤ |s| ≤ 5`** —— 所以规模这一维在这道题上根本不存在,
// ⚠ 而这也意味着:**这道题的输入空间小到可以全枚举**(见 p5015All.cpp)。
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
if (!getline(cin, s)) s = "";
int ans = 0;
for (char c : s) if (c != ' ' && c != '\r') ans++; // ⚠ \r 是给带 CRLF 的输入留的保险
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p5015Char.cpp★ 另一种正确写法:逐字符,空格和换行都排掉

2⚠ 三个错法

✗ 错法一:cin >> s —— 它在回答另一道题

cin >> s 读到第一个空格就停。⇒ 它恒等于在答:「标题里第一个空格之前有几个字符?」

★ 触发条件因此是一句话:标题被空格切成了两段或更多。 ⚠ 而官方样例一(234 )只有一段 ⇒ 它放过这个错法;样例二(Ca 45 )打出 2,当场死。

p5015Cin.cpp✗ 错法一:cin >> s
// P5015 ✗ 错法一:`cin >> s`
//
// ⚠ 这是这道题上最常见的第一版 —— 它读到第一个空格就停,
// 于是「标题」只剩**第一段**。⇒ 它恒等于在回答另一道题:
// 「标题里**第一个空格之前**有几个字符?」
// ★ 触发条件因此是一句话:**标题被空格切成了两段或更多**。
// ⚠ 而官方样例一(`234 `)只有一段 ⇒ **它放过这个错法**;样例二(`Ca 45 `)当场打死它。
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
if (!(cin >> s)) s = "";
printf("%d\n", (int)s.size());
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
✗ 错法二:逐字符读,只排掉了空格 —— 换行也被数进去了

题面那半句话是两样东西:「空格和换行符不计算在内」。 getchar 会把行末那个换行交给你(getline 会替你吃掉),漏掉 c != '\n'恒多算 1

★ 它是「每一组都错」型:下面那张全枚举表里它是 363 / 363,从长度 1 就开始错。 ⇒ 官方样例一测就死 —— 而连着七道题量到的那条规律说的正是这个: 样例这个过滤器专挑「每一组都错」的,放过「偶尔才错」的。

p5015Nl.cpp✗ 错法二:换行也数进去了
✗ 错法三:只数数字字符 —— 照着「40% 的数据」那一档写的

题面第一档写着「保证输入为数字字符及行末换行符」。照那一句写出来的程序, 在 40% 的测试点上完全正确,另外 60% 全错。

⇒ ★★ 这是「出题人的分档是一件工具」反面用法: 分档告诉你「暴力值多少分」,也同样告诉你「照着这一档写会丢多少分」。 ⚠ 而它是个真实的上当方式:那三档是从上往下读的,第一档最先看见。

p5015Digit.cpp✗ 错法三:只数数字字符

3★★★ 这一页一个生成器都没写 —— 输入空间全枚举得完

p5015All.cpp★ 363 个代表输入全跑一遍,外加一次自检
// P5015 解析页上所有数字的出处 —— ★ 这一页一个生成器都没写。
// ./p5015All 人话版
// ./p5015All csv 给 check:viz 用
//
// ★★★ 为什么不写生成器:题面的三档数据范围**一字不差都写着 `1 ≤ |s| ≤ 5`**
// ⇒ 100% 档的输入空间是「62 个字母数字 + 空格」里挑 1~5 个,
// 一共 Σ 63^L = **1 008 443 583** 个(10.08 亿)—— 已经能枚举了,但还能再小一步:
// ★★ **答案只取决于每个字符的「类别」**(空格 / 字母 / 数字),
// ⇒ 3 类 × 长度 1~5 = **363 个代表输入**就覆盖了那 10.08 亿个。
// ⚠ 而「只取决于类别」这句话是**验过**的(第 ② 段自检):
// 拿真实的 63 个字符枚举长度 1~3 的全部 254 079 个输入,逐个和它的代表比 —— 0 个不同。
//
// ⇒ [「输入空间小的时候,算一遍比对拍又快又充分」](/sol/p1002/)的又一次
// ([第 44 章 P1009](/sol/p1009/) 是 50 个整数、[第 46 章 P5704](/sol/p5704/) 是 26 个字符)。
#include <bits/stdc++.h>
using namespace std;
/* 五个版本,全部写成「输入的原始字节 → 输出」的纯函数(读入方式的差别也在里面) */
static int ansGetline(const string& raw) { // ★ 正解:getline + 排掉空格
int n = 0;
for (char c : raw) { if (c == '\n') break; if (c != ' ' && c != '\r') n++; }
return n;
}
static int ansChar(const string& raw) { // ★ 另一种正确写法:逐字符
int n = 0;
for (char c : raw) if (c != ' ' && c != '\n' && c != '\r') n++;
return n;
}
static int ansCin(const string& raw) { // ✗ cin >> s:只拿第一段
size_t i = 0;
while (i < raw.size() && (raw[i] == ' ' || raw[i] == '\n')) i++;
int n = 0;
while (i < raw.size() && raw[i] != ' ' && raw[i] != '\n') { n++; i++; }
return n;
}
static int ansNl(const string& raw) { // ✗ 换行也数进去
int n = 0;
for (char c : raw) if (c != ' ') n++;
return n;
}
static int ansDigit(const string& raw) { // ✗ 只数数字字符
int n = 0;
for (char c : raw) { if (c == '\n') break; if (c >= '0' && c <= '9') n++; }
return n;
}
static const char CLS[3] = {' ', 'a', '7'}; // 空格 / 字母 / 数字,各取一个代表
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
/* ① 363 个代表输入:三个错法各错几个、从哪个长度开始错 */
int bad[3] = {0, 0, 0};
int firstLen[3] = {0, 0, 0};
int total = 0;
for (int len = 1; len <= 5; len++) {
int pw = 1;
for (int k = 0; k < len; k++) pw *= 3;
for (int code = 0; code < pw; code++) {
string s;
int c = code;
for (int k = 0; k < len; k++) { s.push_back(CLS[c % 3]); c /= 3; }
string raw = s + "\n";
total++;
int ok = ansGetline(raw);
if (ansChar(raw) != ok) { fprintf(stderr, "两种正确写法对不上:[%s]\n", s.c_str()); return 1; }
int w[3] = {ansCin(raw), ansNl(raw), ansDigit(raw)};
for (int i = 0; i < 3; i++)
if (w[i] != ok) { bad[i]++; if (!firstLen[i]) firstLen[i] = len; }
}
}
/* ② 自检:「答案只取决于字符的类别」—— 拿真实字符枚举长度 1~3 的全部输入 */
string alpha = " ";
for (char c = 'a'; c <= 'z'; c++) alpha.push_back(c);
for (char c = 'A'; c <= 'Z'; c++) alpha.push_back(c);
for (char c = '0'; c <= '9'; c++) alpha.push_back(c);
long long checked = 0, mismatch = 0;
for (int len = 1; len <= 3; len++) {
long long pw = 1;
for (int k = 0; k < len; k++) pw *= (long long)alpha.size();
for (long long code = 0; code < pw; code++) {
string s, rep;
long long c = code;
for (int k = 0; k < len; k++) {
char ch = alpha[(size_t)(c % (long long)alpha.size())];
c /= (long long)alpha.size();
s.push_back(ch);
rep.push_back(ch == ' ' ? ' ' : (ch >= '0' && ch <= '9' ? '7' : 'a'));
}
checked++;
string ra = s + "\n", rb = rep + "\n";
if (ansGetline(ra) != ansGetline(rb) || ansCin(ra) != ansCin(rb)
|| ansNl(ra) != ansNl(rb) || ansDigit(ra) != ansDigit(rb)) mismatch++;
}
}
/* ③ 100% 档的真实输入空间有多大 */
long long space = 0, pw = 1;
for (int L = 1; L <= 5; L++) { pw *= 63; space += pw; }
/* ④ 「如果 s 很长」—— 题面把规模这一维锁死了,这里量一下它本来能有多快 */
string big(1000000, 'a');
for (size_t i = 7; i < big.size(); i += 7) big[i] = ' ';
big.push_back('\n');
auto t0 = chrono::steady_clock::now();
volatile int sink = ansGetline(big);
auto t1 = chrono::steady_clock::now();
double bigMs = chrono::duration<double, milli>(t1 - t0).count();
(void)sink;
if (csv) {
printf("total,%d\n", total);
printf("badCin,%d\nbadNl,%d\nbadDigit,%d\n", bad[0], bad[1], bad[2]);
printf("firstCin,%d\nfirstNl,%d\nfirstDigit,%d\n", firstLen[0], firstLen[1], firstLen[2]);
printf("checked,%lld\nmismatch,%lld\n", checked, mismatch);
printf("space,%lld\n", space);
printf("bigFast,%d\n", bigMs < 50.0 ? 1 : 0);
return 0;
}
printf("① 3 类字符 × 长度 1~5 = %d 个代表输入(覆盖 100%% 档的全部 %lld 个输入)\n", total, space);
printf(" ✗ cin >> s 错 %3d / %d 个,从长度 %d 起开始出现反例\n", bad[0], total, firstLen[0]);
printf(" ✗ 换行也数进去 错 %3d / %d 个,从长度 %d 起开始出现反例\n", bad[1], total, firstLen[1]);
printf(" ✗ 只数数字字符 错 %3d / %d 个,从长度 %d 起开始出现反例\n", bad[2], total, firstLen[2]);
printf("② 自检「答案只取决于字符的类别」:真实字符长度 1~3 共 %lld 个输入,对不上 %lld 个\n",
checked, mismatch);
printf("③ 顺带量一下「如果 s 很长」:10⁶ 个字符数一遍 %.2f 毫秒 —— 而题面把 |s| 锁在 5\n", bigMs);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 10.08 亿 → 363,而中间那一步是验过的

题面 100% 档的输入空间是「62 个字母数字 + 空格」里挑 1~5 个:

   Σ 63^L (L = 1..5) = 63 + 3969 + 250047 + 15752961 + 992436543 = 1 008 443 583

10 亿 —— 已经能枚举了(第 46 章 P1100 那页枚举过 42.9 亿),但还能再小一步: ★★ 答案只取决于每个字符的「类别」(空格 / 字母 / 数字), ⇒ 3 类 × 长度 1~5 = 363 个代表输入就覆盖了那 10.08 亿个。

⚠⚠ 而「只取决于类别」这句话不能想当然,它是验过的 —— 拿真实的 63 个字符枚举长度 1~3 的全部 254 079 个输入,逐个和它的代表比:0 个不同。 ⇒ ★★ 第 19 章 P2240 立的那条规矩(报「一个反例都没有」之前, 先证明这段代码是活的)在这儿换了个位置用:先证明「压缩」这一步没丢东西,再去枚举压缩后的空间。

363 个代表输入上的结果:

版本 错几个 / 363 从哪个长度开始错
getline 版 ↔ getchar 0(两种正确写法逐个相同)
cin >> s 160 3
✗ 换行也数进去 363(一个不落) 1
✗ 只数数字字符 301 1

★★ 「从哪个长度开始错」是三个精确的整数第 44 章 P1009第 45 章 P1786 那两页也是这么量的)。而 cin >> s 那个 3 有一行证明: 标题要被空格切成两段,最短就是「字符 + 空格 + 字符」。

4⚠⚠ 那三行 |s| ≤ 5 —— 规模这一维在这道题上根本不存在

★★ 题面把三档的上界写成了同一个数,而这不是笔误

40% / 80% / 100% 三档,|s| 的上界一字不差都是 5。 ⇒ 那三档分的不是规模,是字符集(只有数字 → 加上字母 → 再加上空格)。 ★ 换句话说:出题人分档分的是「你要处理几类字符」,而不是「你的程序要多快」。

⚠ 而这件事顺手把一整类顾虑消掉了:这道题不存在「会不会超时」。 顺手量一下就知道 —— 把 |s| 放大 20 万倍到 10⁶,数一遍只要 0.45 毫秒

⇒ ★★ 所以这一页和隔壁那道 P1308 正好凑成一对: 同一张题单里,一道的关卡是「读对」,另一道的关卡也是「读对」 —— 而两道题的读入优化账都是负结论(P1308 顶格 10⁶ 也只花 10.6 毫秒 / 时限 1 秒)。

5★ 官方样例:两组把三个错法全打死 —— 和隔壁那道正好相反

★★ 「样例是一测就死的过滤器」,这次它筛得干干净净
cin >> s ✗ 换行也数 ✗ 只数数字
样例一 234 → 3 放过(3) (4) 放过(3)
样例二 Ca 45 → 4 (2) (5) (2)

★ 两组样例合起来把三个错法全部打死,而且每一个「死在哪一组」都能说出原因: cin >> s两段(样例一只有一段)、只数数字要非数字字符(样例一全是数字)、 换行那个则是每组都死。

⇒ ★★★ 而同一张题单里的 P1308 是另一个极端:两组样例各只打死一个, 剩下两个两组都碰不到。⇒ 「官方样例挡不挡得住」的主语从来是那几组样例的结构, 不是题目难不难 —— 这一章一口气给了两个相反的例子。

6★ 哪一版就已经能过了

★ 第一版写对了就是终点
版本 结果 说明
p5015.cppgetline AC 三行
p5015Char.cppgetchar AC 363 个代表输入上和上面逐个相同
cin >> s WA 样例二就死;全枚举 160 / 363
✗ 换行也数进去 WA 样例一就死;全枚举 363 / 363
✗ 只数数字字符 40 分 照着第一档写的

⇒ ★★ 一句话带走:这道题唯一的考点是「把一行完整地读进来」, 而它值得单独占一页的理由是另一件事 —— 它是全书里输入空间最容易被压小的一道:10.08 亿个输入,363 个代表就问完了, 而那一步压缩本身是验过的。