17 位运算与状态压缩
GESP 3 级NOIP 核心CSP-S
级别标注:GESP 3 级(位运算,考级要求)/ NOIP 核心(竞赛进阶:状态压缩 DP、位运算优化、集合表示)。
一、位运算基础
C++ 常用位运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
& |
按位与 | 6 & 3 = 2(110&011=010) |
\| |
按位或 | 6 | 3 = 7 |
^ |
按位异或 | 6 ^ 3 = 5(1^1=0, 1^0=1) |
~ |
按位取反 | ~5 |
<< |
左移(×2^k) | 1 << 3 = 8 |
>> |
右移(÷2^k) | 8 >> 2 = 2 |
C++ 实例
#include <iostream>
using namespace std;
int main() {
int a = 6, b = 3; // 6=110(2), 3=011(2)
cout << (a & b) << endl; // 2 按位与:110 & 011 = 010
cout << (a | b) << endl; // 7 按位或:110 | 011 = 111
cout << (a ^ b) << endl; // 5 按位异或:110 ^ 011 = 101
cout << (1 << 5) << endl; // 32,左移一位等于乘 2(1<<5 = 2^5)
cout << (32 >> 2) << endl; // 8,右移一位等于整除 2(32>>2 = 8)
return 0;
}必备的小技巧:
- 判断第 k 位是否 1:(x >> k) & 1
- 把第 k 位置 1:x |= (1 << k)
- 把第 k 位清 0:x &= ~(1 << k)
- 翻转第 k 位:x ^= (1 << k)
- lowbit(x) = x & (-x):取最低位的 1(树状数组核心,适合进阶)。
- 判断奇偶:x & 1。
左移内存注意
C++ 实例
// 集合表示:用整型的第 i 位表示"元素 i 是否在集合里"
int mask = 0; // 空集
mask |= (1 << 2); // 加入元素 2(把第 2 位置 1)
mask |= (1 << 3); // 加入元素 3(把第 3 位置 1)
// 判断元素是否在集合:右移后与 1 相与
cout << ((mask >> 2) & 1) << endl; // 1(元素 2 在集合)
cout << ((mask >> 5) & 1) << endl; // 0(元素 5 不在)形象理解位运算像开关面板上的电灯开关:每个二进制位是一盏灯,1 开 0 关。按位与 = 两盏灯都开才亮;按位或 = 有一盏开就亮;异或 = 两盏灯状态不同才亮(相同熄灭)。
易错点位运算优先级比
== 低!mask & (1 << j) == 0 实际先算 (1<<j)==0。判断第 k 位必须写成 ((mask >> k) & 1) 或整体加括号。这是 GESP 3 级笔试高频陷阱。二、用位运算遍历子集(进阶)
C++ 实例
// 枚举集合 s 的所有非空子集(经典技巧,必背)
void enumSubset(int s) {
for (int sub = s; sub; sub = (sub - 1) & s) {
// 处理子集 sub
// (sub-1)&s:去掉最低位 1 后仍限制在 s 内,依次得到全部子集
}
}形象理解
sub = (sub - 1) & s 像倒着翻字典页码:从全集 s 开始,每次“减一并与 s 求交”跳到下一个更小的子集,跳过所有不在 s 里的位,直到翻完 0。考级视角子集枚举是状态压缩 DP 的基石:
for (int sub = s; sub; sub = (sub - 1) & s) 每轮只枚举 s 的真子集,总复杂度 O(3^n) 而非 O(4^n)。NOIP 提高组常考。三、状态压缩 DP(NOIP 高频)
把"已选集合"压成一个整型做 DP 状态。著名的旅行商问题(TSP)、售货郎:
例:最短 Hamiltonian 路径(从 0 出发恰好经过每个点一次回到 0 的最短距离)
C++ 实例
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
const int INF = 1e9;
int dist[N][N];
int dp[1 << N][N]; // dp[mask][i]:已访问集合 mask,当前在 i 的最小距离
int main() {
int n; cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) cin >> dist[i][j];
for (int mask = 0; mask < (1 << n); mask++) // 初始化全无穷
for (int i = 0; i < n; i++) dp[mask][i] = INF;
dp[1][0] = 0; // 从节点 0 出发,集合中只有 0
for (int mask = 1; mask < (1 << n); mask++) { // 枚举已访问集合
for (int i = 0; i < n; i++) { // 当前所在点
if (!((mask >> i) & 1)) continue; // i 不在集合里则跳过
for (int j = 0; j < n; j++) { // 下一步去 j
if ((mask >> j) & 1 || i == j) continue; // j 已在集合或原地
int nxt = mask | (1 << j); // 新集合 = 旧集合 ∪ {j}
dp[nxt][j] = min(dp[nxt][j], dp[mask][i] + dist[i][j]);
}
}
}
cout << dp[(1 << n) - 1][n - 1] << endl; // 访问完全部点,最后停在 n-1
return 0;
}复杂度 O(2^n · n²)。n ≤ 20 左右适用,就是"状态压缩 + 集合 DP"。
其他状态压缩应用:状压求覆盖方案、棋盘覆盖(1×2 骨牌)、子集 DP、DP 优化中的二进制枚举。
形象理解状态压缩像用一张清单记录“哪些城市已去过”:二进制第 i 位 = 城市 i 是否已访问。
dp[mask][i] = 已按清单 mask 访问、最后停在城市 i 的最短距离。易错点状压 DP 三坑:① 数组大小
1 << n 在 n=25 时内存爆炸(约 3 千多万项),n ≤ 20 才安全;② 转移前用 (mask >> i) & 1 判断 i 是否在集合;③ 下标从 0 开始,读入编号要减一。四、位运算的常见技巧汇总(竞赛实战)
- 快速判断集合 A 是否包含集合 B:
(A & B) == B。 - 集合交/并/差:
A & B、A | B、A & (~B)。 __builtin_popcount(x):统计二进制中 1 的个数(GCC 内置函数)。__builtin_ctz(x):末尾 0 的个数(需注意 x=0 的处理)。- 交换两个数:
a ^= b; b ^= a; a ^= b;(笔试趣味题,日常用 swap 即可)。
形象理解集合操作像买菜清单比对:
(A & B) == B 是“B 需要的东西 A 全买了”;__builtin_popcount 像数清单上打了几个勾。易错点
1 << 31 在 int 里是负数(符号位被顶上去)。需要大范围时写 1LL << k。__builtin_ctz(0) 是未定义行为,调用前先判 0。五、易错点 / 常见坑点
- ⚠️ 左移位数溢出:
1 << 31是负数;1LL << 60才安全放大范围。用1LL << k防止整型溢出。 - ⚠️ 位运算优先级较低:
mask & (1 << j) == 0应写成((mask & (1 << j)) == 0),务必加括号。 - ⚠️
~对一个 int 取反,x &= ~(1<<k)里~(1<<k)是 int 的,注意符号位。 - ⚠️ 状态压缩 DP 数组大小
1 << n在 n 稍大时内存爆炸(n=25 就 3 千多万项)。 - ⚠️ 负数右移是算术右移(补符号位),行为可能与预期不同。
- ⚠️ 用
int存集合时元素下标要从 0 开始,注意读入编号。
六、考级要求 vs 竞赛进阶
- 考级要求(GESP 3 级):位运算基础(& | ^ ~ << >>)、用位运算做集合表示、二进制枚举。状态压缩 DP 属竞赛进阶内容,不在 GESP 考纲内。
- 竞赛进阶(NOIP/CSP-J):状压 DP(TSP、骨牌覆盖、DAG 路径)、位运算优化(lowbit、submask 枚举)、结合 DP 的常用位技巧;NOIP 提高组高频得分点。
七、课内练习建议
- 用位运算实现:判断奇偶、左移乘法、lowbit。
- 用二进制枚举所有子集(n=5 打印)。
- 手写一个二进制枚举找子集和的题目 N 个物品选一部分使和不超过 C 最大。
- 用状态压缩做 TSP(n ≤ 18)。
- 练习
__builtin_popcount统计 1 的个数。