位运算与 bitset

一个机器字装 64 个 0/1,一次运算顶 64 次——把"除以 64"的加速用进 DP 与图论

一、为什么要学 bitset

很多算法的内层循环在做 0/1 级别的批量操作:背包的可选状态、图的可达点集、集合的交并。std::bitset 把 N 个 0/1 压进 N/64 个机器字,一次 & | ^ ~ << >> 运算同时处理 64 位——复杂度直接除以 64,105 规模瞬间变得可过。它是 NOI 大纲 2025 修订版对提高级新增的考点,适合作为"隐蔽优化"出现在 DP / 图论题中。

生活类比:点名册上有 64 个学生,逐个打钩要 64 笔。bitset 相当于把 64 格印在一张模板上,"盖章"一次全部完成;与、或、异或就是两张模板叠在一起透光看。
普通数组:64 个格子,一次操作处理 1 格 ✎ … 改 64 格要写 64 次 bitset:同样 64 格压进 1 个机器字,一次位运算全改 1 个 unsigned long long = 64 位 一条指令完成 → 快 64 倍
"除以 64" 的加速从哪来:把逐格操作变成整字运算

二、位运算速查(bitset 的手工版)

操作写法用途
取第 i 位(x >> i) & 1状态查询
置位 / 清位x | (1<<i) / x & ~(1<<i)加入 / 移除元素
lowbitx & -x树状数组、枚举集合元素
枚举子集for (t=S; t; t=(t-1)&S)O(3k) 子集遍历
popcount__builtin_popcountll(x)元素个数统计

更完整的状压集合操作见状压 DP。

三、bitset 三大经典用法

① 0/1 背包按位优化

f 的 bitset 第 j 位 = 能否凑出重量 j;每来一件重 w 的物品:f |= f << w

一次移位 + 或运算完成整个内层循环。N=105 的可行性背包从 O(nN) 降到 O(nN/64)。

② 图上可达性传递(bitset 版 Floyd)

reach[u] 的第 v 位 = u 能否到达 v;转移:reach[v] |= reach[u](u→v 有边时)

按拓扑序(或迭代若干轮)做"或传递",n=104 时 Floyd 的 O(n³) 变 O(n³/64) ≈ 1.6×10⁷,轻松通过。

③ 集合交并与计数

两个 bitset 做 & 后 .count(),一次得到交集大小——莫队、二维偏序、查询"共同元素个数"类题目的常用加速。

f 0 1 1 0 1 0 0 1 …(第 j 位:能否凑出 j) f << w 整体左移 w 位 f |(f<<w) 或运算:旧可达 ∪ 加上新物品后可达 一条语句完成"选 / 不选"两种决策的合并
0/1 背包的 bitset 写法:内层循环被一条 f |= f << w 取代

四、模板代码

#include <bitset>
const int N = 100005;
bitset<N> f;
f[0] = 1;
for (int i = 1; i <= n; i++) f |= (f << w[i]);   // 0/1 背包可行性
if (f[m]) puts("YES");
限制:bitset 长度必须是编译期常量。n 不固定时开最大长度,或手写 unsigned long long 数组版(一个循环做整块或运算)。

五、易错点清单

六、练习

配套小测(5 题,即时判分)