0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1100,日期见页头。两边不一致时信原站。
题目描述
给出一个小于 2³² 的非负整数。这个数可以用一个 32 位的二进制数表示(不足 32 位用 0 补足)。
我们称这个二进制数的前 16 位为「高位」,后 16 位为「低位」。将它的高低位交换,我们可以得到一个新的数。
试问这个新的数是多少(用十进制表示)。
例如,数 1314520 用二进制表示为 0000 0000 0001 0100 0000 1110 1101 1000
(添加了 11 个前导 0 补足为 32 位),其中前 16 位为高位,即 0000 0000 0001 0100;
后 16 位为低位,即 0000 1110 1101 1000。将它的高低位进行交换,我们得到了一个新的二进制数
0000 1110 1101 1000 0000 0000 0001 0100。它即是十进制的 249036820。
输入格式
一个小于 2³² 的非负整数。
输出格式
将新的数输出。
数据范围
0 ≤ x < 2³²(题面正文那一句就是全部的数据范围 —— ⚠ 这道题的坑,全长在这一句上)。
时限 1 秒,内存 128000 KB(125 MB)。
输入输出样例
输入
1314520
输出
249036820
1314520 的高 16 位是 0000 0000 0001 0100(= 20),低 16 位是 0000 1110 1101 1000(= 3800)。
换过来就是 3800 × 65536 + 20 = 249036820。
★ 记住这组数 —— 第 ⑤ 步会看到它只挡住三个错法里的一个。
1★ 一行就写完了 —— 而这一页要说的是那一行里每个字为什么必须那样写
x = H H H H L L L L 每个字母代表 4 个二进制位
x >> 16 = 0 0 0 0 H H H H 高的那一半挪下来
x << 16 = L L L L 0 0 0 0 低的那一半挪上去(★ 高出去的 H 被截掉了)
--------------------------------
或起来 = L L L L H H H H <- 答案第 46 章第 8 步那张「七个动作」表里的两个动作(移位 + 或),拼一下就完了。 算法部分到此结束。 下面整整一页讲的是三个和位运算本身无关的东西: 类型宽度、有没有符号、以及输出怎么打。
// P1100 高低位交换 —— 正解:一行,而那一行里每一个字都是被题面逼出来的//// 把 32 位的 x 拆成「高 16 位 H」和「低 16 位 L」,要的是「L H」这个新数://// x = H H H H L L L L (每个字母是 4 个二进制位)// x >> 16= 0 0 0 0 H H H H <- 高的那一半挪到低位// x << 16= L L L L 0 0 0 0 <- 低的那一半挪到高位(★ 高出去的 H 被截掉了)// 或起来 = L L L L H H H H <- 就是答案//// ★★ 三件必须想清楚的事(题面一句都没明说,但三句都在数据范围里):// ① **必须是无符号。** 题面写「小于 2³² 的非负整数」⇒ x 可以到 4294967295,// `int` 存不下([p1100Int.cpp](/sol/p1100/) 演示读进去会变成什么)。// ② **必须正好是 32 位。** 用 `long long` 的话 `x << 16` **不会被截掉**,// 那半个 H 会留在第 32~47 位上([p1100Ll.cpp](/sol/p1100/))。// ⇒ ★ 「截断」在这道题里不是副作用,**它就是算法的一部分**。// ③ **输出也要按无符号打。** 答案可以 ≥ 2³¹([p1100Sd.cpp](/sol/p1100/))。//// ★ 顺带:这里的 `|` 换成 `+` 或 `^` 都对(两半不重叠),见 p1100Add.cpp —— 那是能证的恒等。#include <cstdio>using namespace std;
int main() { unsigned x; if (scanf("%u", &x) != 1) return 0; printf("%u\n", (x >> 16) | (x << 16)); return 0;}点「运行 ▶」看结果
2⚠ 错法一:用 int —— 「32 位嘛,int 就是 32 位」
int 是 32 位没错,可它有一位拿去当符号了 ⇒ 它只到 2147483647,
而题面允许到 4294967295。
⚠ 它的失败方式很安静:C++11 起,cin >> int 读到越界的数时会
把变量置成 INT_MAX 并置 failbit —— 不崩、不报错,还打得出一个像模像样的十位数。
(x = 3000000000 时它打 4294934527,正解是 1577104080。)
⇒ 触发条件只有一层:x ≥ 2³¹ = 2147483648。
3⚠⚠ 错法二:用 long long —— 「怕不够就开大点」,而这道题开大了正好坏事
上面那个错法的自然反应是:「那我用 long long,肯定够了吧」。
动机完全正确,可它把这道题真正依赖的那一步给取消了 ——
32 位: x << 16 高出去的 H 掉进悬崖,正好腾出位置给低位 <- 这就是「交换」
64 位: x << 16 一位都没掉,H 稳稳停在第 32~47 位上
结果 = x × 65536 + (x >> 16),一个 37 位的数⇒ ★★ 「更保险的类型」在这里是错的,因为算法要的恰恰是那次溢出。
★ 触发线又是一个精确的整数:x << 16 只有在 x ≥ 2¹⁶ = 65536 时才会超出 32 位。
⇒ ★ 而这一条正好被官方那唯一一组样例挡住(1314520 远大于 65536)—— 它是这一页三个错法里唯一一个被样例挡住的。
// P1100 ✗ 错法二:用 long long —— 「怕不够就开大点」,而这道题开大了正好坏事//// ★★★ 这一版是这一页最值得看的一个:它的动机完全正确(`unsigned` 记不住的话就用 64 位嘛),// 可这道题要的**恰恰是那次截断**://// x = 1314520 时// 正解(32 位):(x << 16) 高出去的部分被丢掉 ⇒ 0000 1110 1101 1000 0000 0000 0001 0100// 这一版(64 位):(x << 16) 一位都没丢 ⇒ 结果 = x × 65536 + (x >> 16),一个 37 位的数//// ★ 触发线又是一个精确的整数:**x ≥ 2¹⁶ = 65536**(`x << 16` 只有在 x ≥ 65536 时才会溢出 32 位)。// ⇒ ★★ 官方那唯一一组样例(1314520)**一测就死** —— 它是这一页三个错法里唯一被样例挡住的。#include <cstdio>using namespace std;
int main() { long long x; if (scanf("%lld", &x) != 1) return 0; printf("%lld\n", (x >> 16) | (x << 16)); // ⚠ 少了「只留低 32 位」这一步 return 0;}点「运行 ▶」看结果
4⚠ 错法三:算得全对,输出打成了有符号
类型、移位、掩码一个字都没错,只有 printf 的格式串写成了 %d。
答案一旦 ≥ 2³¹,打出来就是个负数。
★ 而「答案 ≥ 2³¹」是一句可以一眼看穿的话:答案的第 31 位来自 x << 16,
也就是 x 的第 15 位。⇒ 触发 ⟺ x & 32768 非零 ⇒ 随机输入下恰好一半会现形。
⚠ 而官方样例 1314520 的第 15 位恰好是 0(它的低 16 位是 3800 = 0000 1110 1101 1000)
⇒ 样例放过它。
5★ 「看着像 bug、其实恒等」:把 | 换成 +
x >> 16 只可能落在第 015 位上,31 位上
⇒ 两个数没有任何一位同时是 1 ⇒ 加法一次都不进位
⇒ 每一位上「或 / 加 / 异或」给的是同一个答案。x << 16(截断之后)只可能落在第 16
⚠ 而「能证」不等于「不用验」—— 下一步把 2³² 个输入全跑了一遍:| / + / ^ 0 组不同。
6★★★ 这道题根本不需要对拍 —— 它的输入空间只有 42.9 亿个数,一秒多就全跑完了
输入是一个 32 位整数,每个版本都是一个纯函数(读一个数、打一个数)。 ⇒ 「这个写法对不对」这个问题可以被穷举掉,不需要任何概率论、不需要种子、不需要轮数。
本机跑完 2³² 个 x 只要 5.5 秒(A 机 · WSL2 · Linux 6.18-microsoft · 2026-09-06 · 独占):
| 版本 | 第一个反例 x |
反例总数 | 占输入空间 |
|---|---|---|---|
✗ 用 int 读 |
★ 2147483648 | 2147483648 | 正好一半 |
✗ 用 long long |
★ 65536 | 4294901760 | 2³² − 65536 |
| ✗ 输出打成有符号 | ★ 32768 | 2147483648 | 正好一半 |
★ | 换成 + |
— | ★ 0 | — |
★ | 换成 ^ |
— | ★ 0 | — |
★★ 三条触发线和第 ②③④ 步推出来的那三个整数一个不差 —— 2147483648 / 65536 / 32768。 ⇒ 这不是「测出来的」,是证出来的又被穷举确认了一遍。
⚠⚠ 而最后两行那两个 0 配了自检才敢写:同一段枚举代码在上面三行里分别抓到 21 亿、 42.9 亿、21 亿个反例 ⇒ 它不是空壳 (第 19 章 P2240 立的那条规矩:报「0 次 / 找不到」之前,先拿一个已知错的东西验它是活的)。
// P1100 ★★★ 把输入空间**整个**跑一遍:42.9 亿个 x,本机一秒多// ./p1100All 人话版// ./p1100All csv 给 check:viz 用//// ★★★ 为什么这一页可以不靠对拍:这道题的输入是**一个** 32 位整数 ⇒ 输入空间正好 2³² = 4294967296 个。// 而每个版本都是一个纯函数(读一个数、打一个数),于是// 「这个写法对不对」这个问题**可以被穷举掉,不需要任何概率论**。// ⇒ [「输入空间小的时候,算一遍比对拍又快又充分」](/sol/p1002/)的又一次 ——// ★ 而这一次「小」是 42.9 亿,它照样只要一秒多。//// 里面同时干了两件事([第 19 章 P2240](/sol/p2240/) 那条规矩):// ① 报三个真错法的**第一个反例**和**反例总数**;// ② 给两个「看着像 bug、其实恒等」的写法(`|`→`+`、`|`→`^`)做自检 ——// **报「0 个反例」之前,先证明这段枚举代码是活的**,而上面那三列就是它的活证。#include <cstdio>#include <chrono>using namespace std;
int main(int argc, char** argv) { bool csv = (argc > 1 && argv[1][0] == 'c'); auto t0 = std::chrono::steady_clock::now();
unsigned long long badInt = 0, badLl = 0, badSd = 0, badAdd = 0, badXor = 0; unsigned firstInt = 0, firstLl = 0, firstSd = 0; bool hasInt = false, hasLl = false, hasSd = false;
unsigned x = 0; do { unsigned ok = (x >> 16) | (x << 16); // 正解
/* ✗ 用 int 读:C++11 起越界的 cin >> int 会把变量置成 INT_MAX */ unsigned u = (x >= 2147483648u) ? 2147483647u : x; unsigned vInt = (u >> 16) | (u << 16);
/* ✗ 用 long long:左移不截断 */ unsigned long long vLl = ((unsigned long long)x >> 16) | ((unsigned long long)x << 16);
/* ✗ 类型全对,输出打成有符号 */ long long vSd = (long long)(int)ok;
/* ★ 两个「其实恒等」的写法 */ unsigned vAdd = (x >> 16) + (x << 16); unsigned vXor = (x >> 16) ^ (x << 16);
if (vInt != ok) { badInt++; if (!hasInt) { hasInt = true; firstInt = x; } } if (vLl != (unsigned long long)ok) { badLl++; if (!hasLl) { hasLl = true; firstLl = x; } } if (vSd != (long long)ok) { badSd++; if (!hasSd) { hasSd = true; firstSd = x; } } if (vAdd != ok) badAdd++; if (vXor != ok) badXor++; x++; } while (x != 0);
double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
if (csv) { printf("space,4294967296\n"); printf("badInt,%llu\nfirstInt,%u\n", badInt, firstInt); printf("badLl,%llu\nfirstLl,%u\n", badLl, firstLl); printf("badSd,%llu\nfirstSd,%u\n", badSd, firstSd); printf("badAdd,%llu\nbadXor,%llu\n", badAdd, badXor); printf("fastEnough,%d\n", sec < 60.0 ? 1 : 0); return 0; }
printf("P1100 的输入空间只有 2^32 = 4294967296 个数 —— 这里把它们全跑了一遍(%.2f 秒)\n\n", sec); printf(" ✗ 用 int 读 第一个反例 x = %-10u 反例共 %llu 个(正好一半)\n", firstInt, badInt); printf(" ✗ 用 long long 第一个反例 x = %-10u 反例共 %llu 个(= 2^32 - 65536)\n", firstLl, badLl); printf(" ✗ 输出打成有符号 第一个反例 x = %-10u 反例共 %llu 个(正好一半)\n", firstSd, badSd); printf("\n ★ 把 | 换成 +:反例 %llu 个;换成 ^:反例 %llu 个(两半不重叠 ⇒ 三个运算等价)\n", badAdd, badXor); printf(" ⇒ 上面那三行不是零,所以这两个零不是「代码没在跑」。\n"); return 0;}点「运行 ▶」看结果
7⚠⚠ 那还要生成器干什么 —— 用来看「顺手写的那一档漏得有多干净」
| 档位(每档 300 轮) | ✗ 用 int |
✗ 用 long long |
✗ 打成有符号 |
|---|---|---|---|
0 ★ 顺手写的:rng() % 1000000000 |
★ 0(触发 0) | 300(触发 300) | 144(触发 144) |
| 1 ⚠ 真照题面:整个 32 位都随 | 136(触发 136) | 300(触发 300) | 149(触发 149) |
2 ⚠ 对照档:x < 65536 |
★ 0(触发 0) | ★ 0(触发 0) | 149(触发 149) |
| 3 ★ 九个边界值 | 65(触发 65) | 130(触发 130) | 120(触发 120) |
★★★ 三条读得出来的结论:
- ★★★ 档 0 那个 0 才是这张表的主角。 题面写「小于 2³²」,而顺手写生成器的人
会随手打一个
rng() % 1000000000(「十亿够大了吧」)—— 可 10⁹ 只有 2³¹ 的 46.6%。 ⇒ 「用int」那个错法一辈子全绿。 ⇒ 第 37 章 P3378 那条「顺手写的上界正好卡在 bug 的下面」的又一次, ★ 而这一次上界和触发线之间只差 2.15 倍 —— 近到你根本不会怀疑它。 - ★★ 十二格「满足触发条件的轮数 ≡ 真被抓的轮数」一个不差。
因为三个错法的触发条件都只有一层,而且都能写成一句关于
x的话 (x ≥ 2³¹/x ≥ 65536/x的第 15 位是 1)。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次。 - ⚠ 档 2 那两个 0 一个能证、一个是废话:
x < 65536时x << 16装得进 32 位 ⇒long long那个错法结构上不可能出现;而「用int」那个 0 只是因为这一档更小。 ⇒ 两个长得一样的 0,一个是「它在这一档下不可能错」,一个是「这一档没造到点上」。
8★ 哪一版就已经能过了
| 写法 | 结果 | 官方样例 |
|---|---|---|
★ unsigned + %u |
★ AC | ✓ |
★ unsigned + + 代替 | |
★ AC(全枚举 0 反例) | ✓ |
✗ int 读入 |
✗ WA(x ≥ 2³¹,占一半的输入) |
⚠ 放过 |
✗ long long |
✗ WA(x ≥ 65536,占 99.998%) |
★ 一测就死 |
✗ %d 输出 |
✗ WA(x 的第 15 位是 1,占一半) |
⚠ 放过 |
⇒ ★★★ 这道题的算法是一行,而三个错法全长在同一句题面上:「小于 2³² 的非负整数」。
这一句话里其实写了三件事:
不超过 32 位(⇒ 别用
long long)/ 非负(⇒ 别用int)/ 可以到 4294967295(⇒ 别用%d打)。
⇒ ★★ 这也是第 46 章那句「位运算一律在 unsigned 上做」的现场:
它不是风格偏好,是这道题的三分之三。