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 标题统计 —— 正解:读一整行,数「不是空格」的字符//// ★ 这道题的全部内容就是一句话:**标题在一行里,而这一行含空格** ⇒ 只能 `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;}点「运行 ▶」看结果
2⚠ 三个错法
cin >> s 读到第一个空格就停。⇒ 它恒等于在答:「标题里第一个空格之前有几个字符?」
★ 触发条件因此是一句话:标题被空格切成了两段或更多。
⚠ 而官方样例一(234 )只有一段 ⇒ 它放过这个错法;样例二(Ca 45 )打出 2,当场死。
// 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;}点「运行 ▶」看结果
题面那半句话是两样东西:「空格和换行符不计算在内」。
getchar 会把行末那个换行交给你(getline 会替你吃掉),漏掉 c != '\n' 就恒多算 1。
★ 它是「每一组都错」型:下面那张全枚举表里它是 363 / 363,从长度 1 就开始错。 ⇒ 官方样例一测就死 —— 而连着七道题量到的那条规律说的正是这个: 样例这个过滤器专挑「每一组都错」的,放过「偶尔才错」的。
题面第一档写着「保证输入为数字字符及行末换行符」。照那一句写出来的程序, 在 40% 的测试点上完全正确,另外 60% 全错。
⇒ ★★ 这是「出题人的分档是一件工具」的反面用法: 分档告诉你「暴力值多少分」,也同样告诉你「照着这一档写会丢多少分」。 ⚠ 而它是个真实的上当方式:那三档是从上往下读的,第一档最先看见。
3★★★ 这一页一个生成器都没写 —— 输入空间全枚举得完
// 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;}点「运行 ▶」看结果
题面 100% 档的输入空间是「62 个字母数字 + 空格」里挑 1~5 个:
Σ 63^L (L = 1..5) = 63 + 3969 + 250047 + 15752961 + 992436543 = 1 008 443 58310 亿 —— 已经能枚举了(第 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.cpp(getline) |
AC | 三行 |
★ p5015Char.cpp(getchar) |
AC | 363 个代表输入上和上面逐个相同 |
✗ cin >> s |
WA | 样例二就死;全枚举 160 / 363 |
| ✗ 换行也数进去 | WA | 样例一就死;全枚举 363 / 363 |
| ✗ 只数数字字符 | ⚠ 40 分 | 照着第一档写的 |
⇒ ★★ 一句话带走:这道题唯一的考点是「把一行完整地读进来」, 而它值得单独占一页的理由是另一件事 —— 它是全书里输入空间最容易被压小的一道:10.08 亿个输入,363 个代表就问完了, 而那一步压缩本身是验过的。