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 提高组高频得分点。

七、课内练习建议

  1. 用位运算实现:判断奇偶、左移乘法、lowbit。
  2. 用二进制枚举所有子集(n=5 打印)。
  3. 手写一个二进制枚举找子集和的题目 N 个物品选一部分使和不超过 C 最大。
  4. 用状态压缩做 TSP(n ≤ 18)。
  5. 练习 __builtin_popcount 统计 1 的个数。
本章目录
一、位运算基础 二、用位运算遍历子集(进阶) 三、状态压缩 DP(NOIP 高频) 四、位运算的常见技巧汇总(竞赛实战) 五、易错点 / 常见坑点 六、考级要求 vs 竞赛进阶 七、课内练习建议