0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P5704,日期见页头。两边不一致时信原站。
⚠⚠ 顺带记一笔:第 46 章的题单里,这道题原来写的是「洛谷 B2143 转换字母」
—— 而洛谷 B2143 是进制转换(1 ≤ X ≤ 10⁹,2 ≤ M ≤ 16),和大小写一点关系都没有。
题号和题名都错了,2026-09-06 订正成 P5704。⇒ 这是第七次订正本书自己的题单
(前六次:P1330 / P2712 / P1888 / 第 41 章那两条 / P1255 / P1786)。
★ 而它又一次是被「开工前先数一遍题单」那个动作顺手捡到的。
题目描述
输入一个小写字母,输出其对应的大写字母。例如输入 q[回车] 时,会输出 Q。
数据范围
题面只有上面那一句话 —— 输入保证是一个小写字母。 ⚠ 记住这句「保证」,第 ④ 步整节都在称它的重量。
时限 1 秒,内存 128000 KB(125 MB)。
输入输出样例
输入
q
输出
Q
q 的 ASCII 是 97,Q 是 81。差 16?不对 —— 是 32(97 − 65 = 32,而 81 是 Q)。
★ 别背这些数,下一步会看到它们其实是同一位。
1★ 一句话:小写和大写只差一个二进制位
'a' = 97 = 0b 110 0001 'A' = 65 = 0b 100 0001
'b' = 98 = 0b 110 0010 'B' = 66 = 0b 100 0010
'q' = 113= 0b 111 0001 'Q' = 81 = 0b 101 0001
^ ^
第 5 位是 1 第 5 位是 0整张 ASCII 表就是这么排的:同一个字母的大小写,只差第 5 位(32 = 1 << 5)。
⇒ 于是第 46 章第 8 步那张「七个动作」表里,有两个动作都能干这件事:
| 想干什么 | 写法 | 在这道题上 |
|---|---|---|
| 翻转第 5 位 | c ^ 32 |
★ 正解 |
| 把第 5 位清 0 | c & ~32 |
★ 也对 |
★ 而不用位运算的两种写法(c - 32、toupper(c))在这道题上同样对。
⇒ 所以这一页真正的问题不是「哪个对」,是第 ④ 步那个:它们凭什么一样?
// P5704 字母转换 —— 正解:翻转第 5 位//// ★ 'a' = 97 = 0b110_0001,'A' = 65 = 0b100_0001 ⇒ 它们**只差第 5 位**(32 = 1 << 5)。// 整张 ASCII 表都是这么排的:26 个小写字母和 26 个大写字母一一对应,差值恒为 32。// ⇒ 「转成大写」= 把第 5 位清成 0 = 在**保证输入是小写**的前提下,把第 5 位**翻**一下也行。//// ⚠ 这里有一个和位运算无关、却更容易挂人的坑:`c ^ 32` 的类型是 **int**,// 直接 `cout <<` 会打出数字 81 而不是字母 Q(见 p5704Num.cpp)。// ⇒ ★ 「字符也是整数」这句话是双向的:它能参加算术,**也会被当成整数打印出来**。#include <cstdio>using namespace std;
int main() { int c = getchar(); if (c == EOF) return 0; putchar(c ^ 32); // ★ 翻转第 5 位(题面保证输入是小写字母) putchar('\n'); return 0;}点「运行 ▶」看结果
2⚠ 错法一:算对了,打出来的是数字 —— 「字符也是整数」的另一半
char c; cin >> c;
cout << (c - 32) << '\n'; // 输入 q,打出 81c 是 char,可 c - 32(以及 c ^ 32、c & ~32)的类型都是 int ——
C++ 的整型提升会先把 char 变成 int 再算。于是 cout 看到的是一个整数,
它就老老实实打了个 81。
⇒ ★★ 「字符也是整数」这句口号是双向的:
前一半(字符能参加算术)是你主动用的,后一半(结果变回整数了)是它自己发生的。
⇒ 补一个 (char) 或者干脆用 putchar。
★ 触发条件:每一组输入都错,和输入是什么完全无关。
// P5704 ✗ 错法一:算对了,打出来的却是数字//// ★★ `c` 是 `char`,可 `c - 32`(以及 `c ^ 32`、`c & ~32`)的类型都是 **int** ——// C++ 的整型提升会把 `char` 先变成 `int` 再算。于是 `cout << (c - 32)` 打的是 **81**,不是 `Q`。//// ⇒ 这是「字符也是整数」这句话的**另一半**,而且它比前一半更容易咬人:// 前一半(字符能参加算术)是你主动用的,后一半(结果变回整数了)是它自己发生的。// ★ 触发条件:**每一组输入都错** —— 它和输入是什么完全无关。#include <iostream>using namespace std;
int main() { char c; if (!(cin >> c)) return 0; cout << (c - 32) << '\n'; // ⚠ 少了一个 (char) return 0;}点「运行 ▶」看结果
3⚠ 错法二:方向记反了
ASCII 表上大写在前、小写在后('A' = 65 < 'a' = 97),所以「小写转大写」是减 32。
记成加 32 就跑到 129 以上去了,打出来是乱码。
⇒ ★ 加减法要记方向,而「翻转第 5 位」和「把第 5 位清 0」没有方向。 这就是位运算在这道题上唯一实打实的好处 —— 和快慢无关。
4★★★ 四种写法在这道题上完全一样 —— 而它们的差别整个长在题面保证之外
// P5704 ★ 输入空间只有 26 个字符 —— 所以这一页一个生成器都没写// ./p5704All 人话版// ./p5704All csv 给 check:viz 用//// ★★★ 题面只有一句话:「输入一个小写字母」⇒ 输入空间就是 26 个字符。// ⇒ [「输入空间小的时候,算一遍比对拍又快又充分」](/sol/p1002/)的又一次// ([第 44 章 P1009](/sol/p1009/) 那次是 50 个整数,这次是 **26 个字符**)。//// 这份程序干两件事:// ① 在**题面保证的 26 个输入**上,把四种写法(`^32` / `-32` / `&~32` / `toupper`)比一遍;// ② ★★ **把那句保证放开**(换成全部 95 个可见 ASCII),再比一遍 ——// 同一个动作干两件事:给上面那个「全部一致」做自检 + **称出那句保证的分量**// ([第 22 章 P1439](/sol/p1439/) 立的那条)。#include <cstdio>#include <cctype>using namespace std;
int main(int argc, char** argv) { bool csv = (argc > 1 && argv[1][0] == 'c');
auto vXor = [](int c) { return c ^ 32; }; auto vSub = [](int c) { return c - 32; }; auto vAnd = [](int c) { return c & ~32; }; auto vUp = [](int c) { return toupper(c); };
int sameLower = 0; // 26 个小写字母里四种写法一致的个数 for (int c = 'a'; c <= 'z'; c++) if (vXor(c) == vSub(c) && vSub(c) == vAnd(c) && vAnd(c) == vUp(c)) sameLower++;
/* 放开题面那句保证:全部 95 个可见 ASCII(32 ~ 126)*/ int wide = 0, diffAny = 0, diffXor = 0, diffSub = 0, diffAnd = 0; int upperXor = 0, upperAnd = 0; // 输入本来就是大写时,两种写法各错几个 for (int c = 32; c <= 126; c++) { wide++; int ref = vUp(c); // 拿「题目真正想要的语义」当参照物 bool bx = vXor(c) != ref, bs = vSub(c) != ref, ba = vAnd(c) != ref; if (bx) diffXor++; if (bs) diffSub++; if (ba) diffAnd++; if (bx || bs || ba) diffAny++; if (c >= 'A' && c <= 'Z') { if (bx) upperXor++; if (ba) upperAnd++; } }
if (csv) { printf("space,26\n"); printf("sameLower,%d\n", sameLower); printf("wide,%d\n", wide); printf("diffAny,%d\n", diffAny); printf("diffXor,%d\ndiffSub,%d\ndiffAnd,%d\n", diffXor, diffSub, diffAnd); printf("upperXor,%d\nupperAnd,%d\n", upperXor, upperAnd); return 0; }
printf("P5704 的输入空间只有 26 个字符(题面:「输入一个小写字母」)\n\n"); printf(" ★ 题面保证之内('a'~'z',26 个):四种写法一致的有 %d / 26\n\n", sameLower); printf(" ⚠ 把那句保证放开(全部 %d 个可见 ASCII),拿 toupper 的语义当参照物:\n", wide); printf(" c ^ 32 错 %d 个(★ 其中大写字母 %d 个 —— 它把大写翻回了小写)\n", diffXor, upperXor); printf(" c - 32 错 %d 个(★ 最脆的一个:非字母全废)\n", diffSub); printf(" c & ~32 错 %d 个(★ 其中大写字母 %d 个 —— 清零是幂等的)\n", diffAnd, upperAnd); printf(" 至少有一种写法出错的字符:%d / %d\n", diffAny, wide); printf("\n ⇒ 三种写法的差别**整个长在题面保证之外** —— 保证之内它们是同一个函数。\n"); return 0;}点「运行 ▶」看结果
题面只有一句话:「输入一个小写字母」。⇒ 输入空间就是 26 个字符, 把它们全跑一遍比写任何生成器都快、都全。 ⇒ 「输入空间小的时候,算一遍比对拍又快又充分」的又一次 (第 44 章 P1009 那次是 50 个整数,这次是 26 个字符 —— 本书目前最小的一次)。
结果:c ^ 32 / c - 32 / c & ~32 / toupper(c) 在 26 个输入上 26 / 26 全部一致。
⚠⚠ 而「一致」有两种(第 14 章那条:都算对了,还是都没算)。
所以同一段代码顺手干了第二件事:把题面那句保证放开 ——
换成全部 95 个可见 ASCII,拿 toupper 的语义当参照物再比一遍:
| 写法 | 95 个可见字符里错几个 | 其中「输入本来就是大写」那 26 个 |
|---|---|---|
c - 32 |
69 | 26(打出控制字符) |
c ^ 32 |
69 | ★ 26 —— 它把大写翻回了小写 |
c & ~32 |
★ 37 | ★ 0 —— 清零是幂等的 |
toupper(c) |
0(它就是参照物) | 0 |
★★★ 三条读得出来的结论:
- ★★★ 那个「26 / 26 全一致」不是空壳 —— 同一段枚举代码在放开之后 立刻抓出 69 / 69 / 37 个不同。⇒ 第 19 章 P2240 立的那条规矩: 报「完全一致」之前,先证明这段代码是活的。
- ★★ 同一个动作顺带称出了那句保证的分量 —— 它是命门: 一旦输入可能不是小写字母,三种写法立刻分成三个不同的答案 (第 22 章 P1439 那次同形:放开题面那句约束,同时做完自检和称重)。
- ★★ 而「哪个写法更稳」和「哪个写法更对」是两件事。
在这道题上四种写法并列第一;出了这道题,
& ~32比^ 32稳 (幂等:已经是大写就不动它),而toupper比两个都稳。 ⇒ ★ 位运算那两行的价值在于说得清它在动哪一位,不在于它更保险。
5★ 哪一版就已经能过了
| 写法 | 结果 | 出了这道题还稳吗 |
|---|---|---|
★ c ^ 32 |
AC | ⚠ 输入已是大写会被翻回小写 |
★ c & ~32 |
AC | ★ 幂等,26 个大写字母一个都不动 |
★ c - 32 |
AC | ⚠ 非字母全废 |
★ toupper(c) |
AC | ★ 最稳(它就是为这件事写的) |
✗ cout << (c - 32) |
WA(打出 81) | — |
✗ c + 32 |
WA(乱码) | — |
⇒ ★★ 这道题两分钟就能过,而它值得单独占一页的理由只有一条: 它是全书里「一个整数就是一排开关」这句话最短的证据 —— 大小写这件看起来完全属于「文字」的事,落到机器里只是第 5 位是 0 还是 1。
⇒ 记住这一条,第 47 章那些字符处理就都有了同一个底: 字符没有什么特别的,它就是一个小整数。