题单 · 习题解析

洛谷 P1100 高低位交换

★★★ 输入空间只有 2³² = 4294967296 个数、每个版本又都是纯函数 ⇒ **这道题根本不需要对拍,全枚举只要 5.5 秒** —— [「输入空间小的时候算一遍又快又充分」](/sol/p1002/)的又一次,而这次「小」是 42.9 亿;★★★ 三个错法的触发线是三个**精确的整数**,而且是先证后枚举确认的:用 `int` 读 → **2147483648**(反例占一半)/ 用 `long long` → **65536**(左移不截断,反例 4294901760 个)/ `%d` 输出 → **32768**(`x` 的第 15 位);★★ 「更保险的类型」在这里是错的 —— **这道题要的恰恰是那次 32 位截断,截断就是算法本身**;★★★ 顺手写的生成器 `rng() % 1000000000` 让「用 int」变成**精确的 0**(10⁹ 只有 2³¹ 的 46.6%,上界和触发线只差 **2.15 倍**,近到你不会怀疑它)—— [第 37 章 P3378](/sol/p3378/) 那条的又一次;★★ 十二格「触发 ≡ 抓获」一个不差;★ 把 `|` 换成 `+` 或 `^` 是**能证的恒等**(两半不重叠),全枚举 0 反例,而这个 0 配了自检;⚠ 官方唯一那组样例只挡住三个错法里的一个

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

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

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

2⚠ 错法一:用 int —— 「32 位嘛,int 就是 32 位」

★ 触发线是一个精确的整数:2147483648

int 是 32 位没错,可它有一位拿去当符号了 ⇒ 它只到 2147483647, 而题面允许到 4294967295

⚠ 它的失败方式很安静:C++11 起,cin >> int 读到越界的数时会 把变量置成 INT_MAX 并置 failbit —— 不崩、不报错,还打得出一个像模像样的十位数。 (x = 3000000000 时它打 4294934527,正解是 1577104080。)

⇒ 触发条件只有一层:x ≥ 2³¹ = 2147483648

p1100Int.cpp✗ 错法一:int 存不下 2³²

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)—— 它是这一页三个错法里唯一一个被样例挡住的。

p1100Ll.cpp✗ 错法二:long long ⇒ 左移不截断(样例一测就死)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

4⚠ 错法三:算得全对,输出打成了有符号

★★ 触发条件落在输入的某「一个二进制位」上

类型、移位、掩码一个字都没错,只有 printf 的格式串写成了 %d。 答案一旦 ≥ 2³¹,打出来就是个负数。

★ 而「答案 ≥ 2³¹」是一句可以一眼看穿的话:答案的第 31 位来自 x << 16, 也就是 x 的第 15 位。⇒ 触发 ⟺ x & 32768 非零 ⇒ 随机输入下恰好一半会现形。

⚠ 而官方样例 1314520 的第 15 位恰好是 0(它的低 16 位是 3800 = 0000 1110 1101 1000) ⇒ 样例放过它

p1100Sd.cpp✗ 错法三:%d 而不是 %u

5★ 「看着像 bug、其实恒等」:把 | 换成 +

★★ 两行能证完,而且这一页有本事把它验到底

x >> 16 只可能落在第 015 位上,x << 16(截断之后)只可能落在第 1631 位上 ⇒ 两个数没有任何一位同时是 1 ⇒ 加法一次都不进位 ⇒ 每一位上「或 / 加 / 异或」给的是同一个答案。

⚠ 而「能证」不等于「不用验」—— 下一步把 2³² 个输入全跑了一遍| / + / ^ 0 组不同

p1100Add.cpp★ 把 | 换成 + —— 照样 AC

6★★★ 这道题根本不需要对拍 —— 它的输入空间只有 42.9 亿个数,一秒多就全跑完了

★★★ 「输入空间小的时候,算一遍比对拍又快又充分」—— 而这次「小」是 4294967296

输入是一个 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 次 / 找不到」之前,先拿一个已知错的东西验它是活的)。

p1100All.cpp★★ 全枚举 2³²:三条触发线 + 两个 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⚠⚠ 那还要生成器干什么 —— 用来看「顺手写的那一档漏得有多干净」

p1100Gen.cpp(四档)顺手写的 / 真照题面 / 小于 65536 / 边界值
★★★ 四档 × 三个错法,十二格「触发 ≡ 抓获」一个不差 —— 而顺手写的那一档漏掉一整个
档位(每档 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)

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

  1. ★★★ 档 0 那个 0 才是这张表的主角。 题面写「小于 2³²」,而顺手写生成器的人 会随手打一个 rng() % 1000000000(「十亿够大了吧」)—— 可 10⁹ 只有 2³¹ 的 46.6%。 ⇒ 「用 int」那个错法一辈子全绿。 ⇒ 第 37 章 P3378 那条「顺手写的上界正好卡在 bug 的下面」的又一次, ★ 而这一次上界和触发线之间只差 2.15 倍 —— 近到你根本不会怀疑它。
  2. ★★ 十二格「满足触发条件的轮数 ≡ 真被抓的轮数」一个不差。 因为三个错法的触发条件都只有一层,而且都能写成一句关于 x 的话 (x ≥ 2³¹x ≥ 65536x 的第 15 位是 1)。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次。
  3. 档 2 那两个 0 一个能证、一个是废话x < 65536x << 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 上做」的现场: 它不是风格偏好,是这道题的三分之三。