题单 · 习题解析

洛谷 P5704 【深基2.例6】字母转换

★★ 全书里「一个整数就是一排开关」最短的证据:大小写这件看起来完全属于「文字」的事,落到机器里只是**第 5 位是 0 还是 1**(`a` = 97 = 0b1100001,`A` = 65 = 0b1000001);★★★ 输入空间只有 **26 个字符** ⇒ 这一页一个生成器都没写(本书目前最小的一次,[第 44 章 P1009](/sol/p1009/) 那次是 50 个整数),四种写法(`^32` / `-32` / `&~32` / `toupper`)**26 / 26 全部一致**;★★★ 而同一个动作顺带称出了题面那句「输入是小写字母」的分量 —— 放开到 95 个可见 ASCII,三种写法立刻错 **69 / 69 / 37** 个(`&~32` 幂等,26 个大写字母一个都不动;`^32` 把大写翻回小写)⇒ **它们的差别整个长在题面保证之外**,而这也正是那个「全部一致」的自检;★★ 最容易咬人的一条和位运算无关:`c ^ 32` 的类型是 `int`,`cout << (c - 32)` 打出 **81** 而不是 `Q` ——「字符也是整数」这句话是**双向**的;⚠⚠ 这道题在第 46 章题单里原来写成「B2143 转换字母」(B2143 其实是**进制转换**),2026-09-06 订正 —— **第七次订正本书自己的题单**

⚠ 先自己写一遍,再往下看

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

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★ 一句话:小写和大写只差一个二进制位

★★ 把 ASCII 写成二进制,那 32 就现形了
   '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 - 32toupper(c))在这道题上同样对。 ⇒ 所以这一页真正的问题不是「哪个对」,是第 ④ 步那个:它们凭什么一样?

p5704.cpp★ 正解:翻转第 5 位
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠ 错法一:算对了,打出来的是数字 —— 「字符也是整数」的另一半

★★ 这一条和位运算无关,而它比位运算本身更容易咬人
   char c;  cin >> c;
   cout << (c - 32) << '\n';      //  输入 q,打出 81

cchar,可 c - 32(以及 c ^ 32c & ~32)的类型都是 int —— C++ 的整型提升会先把 char 变成 int 再算。于是 cout 看到的是一个整数, 它就老老实实打了个 81

⇒ ★★ 「字符也是整数」这句口号是双向的: 前一半(字符能参加算术)是你主动用的,后一半(结果变回整数了)是它自己发生的。 ⇒ 补一个 (char) 或者干脆用 putchar。 ★ 触发条件:每一组输入都错,和输入是什么完全无关。

p5704Num.cpp✗ 错法一:cout << (c - 32) 打出 81
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠ 错法二:方向记反了

★ 而它正好说明这一章为什么更愿意写 ^ 32 而不是 ± 32

ASCII 表上大写在前、小写在后'A' = 65 < 'a' = 97),所以「小写转大写」是 32。 记成加 32 就跑到 129 以上去了,打出来是乱码。

⇒ ★ 加减法要记方向,而「翻转第 5 位」和「把第 5 位清 0」没有方向。 这就是位运算在这道题上唯一实打实的好处 —— 和快慢无关。

p5704Add.cpp✗ 错法二:c + 32

4★★★ 四种写法在这道题上完全一样 —— 而它们的差别整个长在题面保证之外

p5704All.cpp★ 26 个输入全跑一遍,再把题面那句保证放开
// 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 个字符」⇒ 这一页一个生成器都没写

题面只有一句话:「输入一个小写字母」。⇒ 输入空间就是 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

★★★ 三条读得出来的结论:

  1. ★★★ 那个「26 / 26 全一致」不是空壳 —— 同一段枚举代码在放开之后 立刻抓出 69 / 69 / 37 个不同。⇒ 第 19 章 P2240 立的那条规矩: 报「完全一致」之前,先证明这段代码是活的。
  2. ★★ 同一个动作顺带称出了那句保证的分量 —— 它是命门: 一旦输入可能不是小写字母,三种写法立刻分成三个不同的答案 (第 22 章 P1439 那次同形:放开题面那句约束,同时做完自检和称重)。
  3. ★★ 而「哪个写法更稳」和「哪个写法更对」是两件事。 在这道题上四种写法并列第一;出了这道题,& ~32^ 32 稳 (幂等:已经是大写就不动它),而 toupper 比两个都稳。 ⇒ ★ 位运算那两行的价值在于说得清它在动哪一位,不在于它更保险。
p5704Sub.cpp★ 另一种正确写法:c - 32
p5704And.cpp★ 第三种正确写法:c & ~32(幂等)

5★ 哪一版就已经能过了

★ 第一版就是终点 —— 这道题真正的收获是那张「差一个 bit」的表
写法 结果 出了这道题还稳吗
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 章那些字符处理就都有了同一个底: 字符没有什么特别的,它就是一个小整数。