例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。
练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。
每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。
先把地基摆一遍
地基只有一句话:一个整数就是一排开关。 我们平时把 13 读成「十三」,而计算机里它是 1101 —— 从右往左数,第 0 号开关开、第 1 号关、第 2 号开、第 3 号开。位运算就是拨这排开关的几个动作,它们不是数学运算,别拿加减乘除的直觉去套。
⚠ 两个编号要一次记牢,后面所有条目都按这个约定:最右边那一位是「第 0 位」(不是第 1 位),而且越往左位数越大。第 i 位代表的数值是 2ⁱ —— 所以 1101 = 8 + 4 + 0 + 1 = 13。
★ 而这排开关彼此没有一根线连着:对第 3 位做的事不会影响第 2 位。这就是为什么「一个集合」可以用「一个整数」表示(第 28 章状压 DP 的全部立论),也是为什么第 46 章那道题能用异或一路消下去。
★★ 优先级是这一组唯一一个会咬人的语法问题:位运算的优先级比比较运算还低。所以凡是位运算和 == / < 写在一起,一律加括号 —— 这条没有例外,看到就加。
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。
例题 ① 一排开关
给你一个非负整数 x,请把它的二进制从低位到高位逐位打印出来,再数一数里面一共有几个 1。
⚠ 位的编号从 0 开始,最右边那一位是第 0 位。所以 13 的二进制 1101 里,第 0 位是 1、第 1 位是 0、第 2 位是 1、第 3 位是 1。
x 和 k(0 ≤ x < 2³¹,1 ≤ k ≤ 31)。k 行,第 i 行形如 i: b,b 是 x 的第 i 位(0 或 1);最后一行一个整数,表示 x 的二进制里 1 的个数。输入
13 4
输出
0: 1 1: 0 2: 1 3: 1 3
1101。所以第 0、2、3 位是 1,第 1 位是 0,一共三个 1。「取出第 i 位」只有一种写法:先把它挪到最右边,再只留最右边那一位。
// 例题 ①「一排开关」—— 位运算组//// 这道题只要两件事:把某一位取出来(x >> i & 1),数一数有几个 1。// 它是第 3 章「二进制枚举」和第 28 章「状压 DP」每一行都在做的动作。
#include <bits/stdc++.h>using namespace std;
int main() { int x, k; cin >> x >> k;
// 从低位到高位:第 0 位是最右边那一位 for (int i = 0; i < k; i++) { // x >> i 把第 i 位挪到最右边;& 1 只留最右边那一位 int b = x >> i & 1; cout << i << ": " << b << "\n"; }
// 数 1 的个数。自己写也只要一行循环,但内置的这个更快、也更短 cout << __builtin_popcount(x) << "\n"; return 0;}点「运行 ▶」看结果
x >> i—— 整排开关右移 i 格,把你要的那一位挪到最右边。拿 x = 13、i = 2 走一遍:13 >> 2=1101去掉右边两位 =11= 3。& 1—— 只留最右边那一位,其余全抹掉。3 & 1= 1 ⇒ 第 2 位是 1。✓- ⚠ 两步之间不用加括号:
>>的优先级比&高,所以x >> i & 1就是(x >> i) & 1。但只要这句话和==写在一起(比如x >> i & 1 == 1),就必须加括号 —— 这条没有例外。 - 数 1 的个数用
__builtin_popcount(x)。自己写也只要一行循环,但它是一条 CPU 指令,快得多。⚠ 参数是 long long 时要换成__builtin_popcountll。
这两句话就是第 3 章「二进制枚举」和第 28 章「状压 DP」里出现最多的两句。
例题 ② 开关面板
有 n 盏灯,编号 0 ~ n−1,一开始全灭。我们用一个整数 mask 记住它们的状态:mask 的第 i 位是 1 就表示第 i 盏灯亮着。
现在有 m 条操作,请依次执行,并回答其中的查询。
n、m(1 ≤ n ≤ 30)。接下来 m 行,每行是 1 i(开第 i 盏)、2 i(关第 i 盏)、3 i(拨动第 i 盏)或者 4(查询当前有几盏亮着)之一。4 就输出一行,表示当时亮着的灯数;全部操作做完后,再输出一行,表示最终的 mask(十进制)。输入
5 6 1 0 1 3 4 2 0 3 1 4
输出
2 2 10
mask = 1;开 3 号 ⇒ mask = 9(1001);查询 ⇒ 2 盏。关 0 号 ⇒ mask = 8;拨 1 号(原来是灭的 ⇒ 变亮)⇒ mask = 10(1010);查询 ⇒ 还是 2 盏。三个动作各对应一条写法。想清楚每一条为什么「只动第 i 位、其余位原样」。
// 例题 ②「开关面板」—— 位运算组//// 一个整数就是一排开关,于是「一个集合」可以只用一个 int 存着。// 三个动作各对应一条写法:加入 | 、删除 & ~ 、翻转 ^ 。
#include <bits/stdc++.h>using namespace std;
int main() { int n, m; cin >> n >> m;
int mask = 0; // 一盏灯都没亮 —— 空集合就是 0 for (int t = 0; t < m; t++) { int op, i; cin >> op; if (op == 4) { // 查询:亮着几盏 = 二进制里有几个 1 cout << __builtin_popcount(mask) << "\n"; continue; } cin >> i; if (op == 1) mask |= 1 << i; // 开:把第 i 位设成 1 else if (op == 2) mask &= ~(1 << i); // 关:把第 i 位设成 0 else mask ^= 1 << i; // 拨:把第 i 位翻过来 }
cout << mask << "\n"; return 0;}点「运行 ▶」看结果
- 开:
mask |= 1 << i。|的规则是「两边只要有一个是 1 就是 1」,而1 << i只有第 i 位是 1 ⇒ 第 i 位被顶成 1,其余位是「原来那位 | 0」= 原样。 - 关:
mask &= ~(1 << i)。~把每一位翻过来,于是~(1 << i)是「只有第 i 位是 0、其余全是 1」的掩码;再&一下,第 i 位被按成 0,其余位是「原来那位 & 1」= 原样。 - 拨:
mask ^= 1 << i。^(异或)的规则是「两边不一样才是 1」⇒ 和 1 异或必翻、和 0 异或不变。 - 查:亮着几盏 = 二进制里有几个 1 =
__builtin_popcount(mask)。⇒ 集合的大小根本不用另外存一个变量。
一个整数就装下了一个集合 —— 这正是第 28 章状压 DP 的全部立论。而它成立的理由只有一句:这排开关彼此没有一根线连着,动第 3 位不会影响第 2 位。
用到的写法:x | (1 << i) x & ~(1 << i) x ^ (1 << i) 1 << n __builtin_popcount(x)
练习(3 道)
答案折叠着,先自己写一版。
练习 ① 是不是 2 的幂
给 T 个正整数,逐个判断它是不是 2 的某个次方(1、2、4、8、16…)。
T;接下来 T 行,每行一个正整数 x(1 ≤ x ≤ 10¹⁸)。T 行,是就输出 Yes,不是就输出 No。输入
5 1 2 3 8 12
输出
Yes Yes No Yes No
11、12 = 1100 都有不止一个 1。先把这五个数写成二进制看看:1、2、4、8、16。它们有什么共同点?然后想想 x & -x 取出来的是什么 —— 两件事一凑就出来了。⚠ 还有一件事要注意:x 可以到 10¹⁸。
用到的写法:x & -x (x & 1) == 0
练习 ② 凑出 S 的选法有几种
给 n 个整数,你可以从中任选若干个(也可以一个都不选)。问有多少种选法,使得选中的数之和恰好等于 S。
⚠ 两种选法只要「选中的位置」不同就算不同,即使数值一样。
n 和 S(1 ≤ n ≤ 20);第二行 n 个整数。输入
3 5 1 2 3
输出
1
1 2 3 里和为 5 的只有 {2, 3} 这一种。n ≤ 20 是出题人在告诉你「可以把所有选法都试一遍」——一共有多少种选法?怎么用一个整数 mask 把它们不重不漏地数一遍?
这就是第 3 章的二进制枚举:用一个整数当「选了哪些」的花名册。
用到的写法:1 << n x >> i & 1
练习 ③ 一路抹掉最低位的 1
给一个正整数 x。每一步,把它最低位的那个 1 抹掉(连同它后面的 0 一起看成一个数),直到 x 变成 0 为止。请输出每一步抹掉了多少、抹完还剩多少。
例:12 的二进制是 1100,最低位的那个 1 代表的数是 4,抹掉之后剩 8(1000);再抹一次,抹掉 8,剩 0。
x(1 ≤ x < 2³¹)。x。输入
12
输出
4 8 8 0
12 = 1100 ⇒ 抹掉 4 剩 8,再抹掉 8 剩 0。一共走了两步,正好是 12 里 1 的个数。「最低位的那个 1」有一个专门的名字叫 lowbit,速查里有它的写法。抹掉它就是减去它。
这正是第 38 章树状数组往前跳的那一句 i -= i & -i。顺带一个能自己验的性质:循环次数恰好等于 __builtin_popcount(x)。
用到的写法:x & -x __builtin_popcount(x)
这是第一组 · 回 C++ 速查 · 下一组:整数类型与溢出 →