课程 · C++ 速查 · 位运算

位运算 例题与练习

把「一个整数就是一排开关」这句话,变成手上真的会写的四个动作:取一位、加一位、去一位、数几个 1。

2 道例题3 道练习5 份可运行代码回速查看这一组的 10 条 →
怎么用这一页

例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。

练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。

每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。

先把地基摆一遍

4 段

地基只有一句话:一个整数就是一排开关。 我们平时把 13 读成「十三」,而计算机里它是 1101 —— 从右往左数,第 0 号开关开、第 1 号关、第 2 号开、第 3 号开。位运算就是拨这排开关的几个动作,它们不是数学运算,别拿加减乘除的直觉去套。

⚠ 两个编号要一次记牢,后面所有条目都按这个约定:最右边那一位是「第 0 位」(不是第 1 位),而且越往左位数越大。第 i 位代表的数值是 2ⁱ —— 所以 1101 = 8 + 4 + 0 + 1 = 13。

★ 而这排开关彼此没有一根线连着:对第 3 位做的事不会影响第 2 位。这就是为什么「一个集合」可以用「一个整数」表示(第 28 章状压 DP 的全部立论),也是为什么第 46 章那道题能用异或一路消下去。

★★ 优先级是这一组唯一一个会咬人的语法问题:位运算的优先级比比较运算还低。所以凡是位运算和 == / < 写在一起,一律加括号 —— 这条没有例外,看到就加。

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。

例题 ① 一排开关

bitEx1.cpp

给你一个非负整数 x,请把它的二进制从低位到高位逐位打印出来,再数一数里面一共有几个 1。

⚠ 位的编号从 0 开始,最右边那一位是第 0 位。所以 13 的二进制 1101 里,第 0 位是 1、第 1 位是 0、第 2 位是 1、第 3 位是 1。

输入格式一行两个整数 xk0 ≤ x < 2³¹1 ≤ k ≤ 31)。
输出格式k 行,第 i 行形如 i: bbx 的第 i 位(0 或 1);最后一行一个整数,表示 x 的二进制里 1 的个数。

输入

13 4

输出

0: 1
1: 0
2: 1
3: 1
3
13 = 8 + 4 + 1 = 1101。所以第 0、2、3 位是 1,第 1 位是 0,一共三个 1。
先想一想

「取出第 i 位」只有一种写法:先把它挪到最右边,再只留最右边那一位

bitEx1.cpp参考代码
// 例题 ①「一排开关」—— 位运算组
//
// 这道题只要两件事:把某一位取出来(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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. x >> i —— 整排开关右移 i 格,把你要的那一位挪到最右边。拿 x = 13、i = 2 走一遍:13 >> 2 = 1101 去掉右边两位 = 11 = 3。
  2. & 1 —— 只留最右边那一位,其余全抹掉。3 & 1 = 1 ⇒ 第 2 位是 1。✓
  3. ⚠ 两步之间不用加括号:>> 的优先级比 & 高,所以 x >> i & 1 就是 (x >> i) & 1。但只要这句话和 == 写在一起(比如 x >> i & 1 == 1),就必须加括号 —— 这条没有例外。
  4. 数 1 的个数用 __builtin_popcount(x)。自己写也只要一行循环,但它是一条 CPU 指令,快得多。⚠ 参数是 long long 时要换成 __builtin_popcountll
记住这一句

这两句话就是第 3 章「二进制枚举」和第 28 章「状压 DP」里出现最多的两句。

用到的写法:x >> i & 1 __builtin_popcount(x) (x & 1) == 0

例题 ② 开关面板

bitEx2.cpp

n 盏灯,编号 0 ~ n−1,一开始全灭。我们用一个整数 mask 记住它们的状态:mask 的第 i 位是 1 就表示第 i 盏灯亮着。

现在有 m 条操作,请依次执行,并回答其中的查询。

输入格式第一行两个整数 nm1 ≤ 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
开 0 号 ⇒ mask = 1;开 3 号 ⇒ mask = 91001);查询 ⇒ 2 盏。关 0 号 ⇒ mask = 8;拨 1 号(原来是灭的 ⇒ 变亮)⇒ mask = 101010);查询 ⇒ 还是 2 盏。
先想一想

三个动作各对应一条写法。想清楚每一条为什么「只动第 i 位、其余位原样」。

bitEx2.cpp参考代码
// 例题 ②「开关面板」—— 位运算组
//
// 一个整数就是一排开关,于是「一个集合」可以只用一个 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. mask |= 1 << i| 的规则是「两边只要有一个是 1 就是 1」,而 1 << i 只有第 i 位是 1 ⇒ 第 i 位被顶成 1,其余位是「原来那位 | 0」= 原样。
  2. mask &= ~(1 << i)~ 把每一位翻过来,于是 ~(1 << i) 是「只有第 i 位是 0、其余全是 1」的掩码;再 & 一下,第 i 位被按成 0,其余位是「原来那位 & 1」= 原样。
  3. mask ^= 1 << i^(异或)的规则是「两边不一样才是 1」⇒ 和 1 异或必翻、和 0 异或不变。
  4. :亮着几盏 = 二进制里有几个 1 = __builtin_popcount(mask)。⇒ 集合的大小根本不用另外存一个变量。
记住这一句

一个整数就装下了一个集合 —— 这正是第 28 章状压 DP 的全部立论。而它成立的理由只有一句:这排开关彼此没有一根线连着,动第 3 位不会影响第 2 位。

用到的写法:x | (1 << i) x & ~(1 << i) x ^ (1 << i) 1 << n __builtin_popcount(x)

练习(3 道)

答案折叠着,先自己写一版。

练习 ① 是不是 2 的幂

bitTry1.cpp

T 个正整数,逐个判断它是不是 2 的某个次方(1、2、4、8、16…)。

输入格式第一行一个整数 T;接下来 T 行,每行一个正整数 x1 ≤ x ≤ 10¹⁸)。
输出格式T 行,是就输出 Yes,不是就输出 No

输入

5
1
2
3
8
12

输出

Yes
Yes
No
Yes
No
1 = 2⁰、2 = 2¹、8 = 2³ 都算;3 = 11、12 = 1100 都有不止一个 1。
提示

先把这五个数写成二进制看看:1、2、4、8、16。它们有什么共同点?然后想想 x & -x 取出来的是什么 —— 两件事一凑就出来了。⚠ 还有一件事要注意:x 可以到 10¹⁸。

bitTry1.cpp参考答案

用到的写法:x & -x (x & 1) == 0

练习 ② 凑出 S 的选法有几种

bitTry2.cpp

n 个整数,你可以从中任选若干个(也可以一个都不选)。问有多少种选法,使得选中的数之和恰好等于 S

⚠ 两种选法只要「选中的位置」不同就算不同,即使数值一样。

输入格式第一行两个整数 nS1 ≤ n ≤ 20);第二行 n 个整数。
输出格式一行一个整数,表示方案数。

输入

3 5
1 2 3

输出

1
1 2 3 里和为 5 的只有 {2, 3} 这一种。
提示

n ≤ 20 是出题人在告诉你「可以把所有选法都试一遍」——一共有多少种选法?怎么用一个整数 mask 把它们不重不漏地数一遍?

bitTry2.cpp参考答案
记住这一句

这就是第 3 章的二进制枚举:用一个整数当「选了哪些」的花名册

用到的写法:1 << n x >> i & 1

练习 ③ 一路抹掉最低位的 1

bitTry3.cpp

给一个正整数 x。每一步,把它最低位的那个 1 抹掉(连同它后面的 0 一起看成一个数),直到 x 变成 0 为止。请输出每一步抹掉了多少、抹完还剩多少。

例:12 的二进制是 1100,最低位的那个 1 代表的数是 4,抹掉之后剩 8(1000);再抹一次,抹掉 8,剩 0。

输入格式一行一个正整数 x1 ≤ x < 2³¹)。
输出格式若干行,每行两个整数:这一步抹掉的值、以及抹完之后的 x

输入

12

输出

4 8
8 0
12 = 1100 ⇒ 抹掉 4 剩 8,再抹掉 8 剩 0。一共走了两步,正好是 12 里 1 的个数。
提示

「最低位的那个 1」有一个专门的名字叫 lowbit,速查里有它的写法。抹掉它就是减去它。

bitTry3.cpp参考答案
记住这一句

这正是第 38 章树状数组往前跳的那一句 i -= i & -i。顺带一个能自己验的性质:循环次数恰好等于 __builtin_popcount(x)

用到的写法:x & -x __builtin_popcount(x)

做完了去哪儿

这是第一组 · 回 C++ 速查 · 下一组:整数类型与溢出 →