1568 字
8 分钟
竞赛算法(3) 位运算
简单的跳过, 从典型trick开始.
lowbit
inline u32 lowbit(u32 x) { return x & (-x);}分类求缺失数
数组中只有2种数出现次数为奇数, 其他数出现次数均为偶数, 找到出现次数为奇数的数.
- 设这两个数为a和b.
- 取异或和sum, 则显然sum = a ^ b.
- sum中至少存在一个1, 我们取任意一个1. 这里方便起见取一定存在的lowbit(sum).
- lowbit(sum)一定由0和1异或而成, 所以a和b在lowbit(sum)这位上必然一个为0一个为1.
- 我们将所有数分类成lowbit(sum)位上为0的数组arr0和为1的数组arr1, 则arr0中只有一个出现奇数次的数, arr1中只有一个出现奇数次的数. 问题转化为在数组中寻找唯一的出现次数为奇数的数. 取异或和即可.
判断2的整幂
bool is2pow(u32 n) { return n == 0 ? 0 : n == lowbit(n);}
// 另一种写法bool is2pow(u32 n) { return n == 0 ? 0 : __builtin_popcount(n) == 1;}判断n的整幂
bool is3pow(u32 n) { return n == 0 ? 0 : (1162261467 % n == 0); // 其中1162261467是int范围内最大的3的整数次幂. 其它数字同理.}将最高位后的所有位填充为1
inline u32 highbit(u32 n) { if (n == 0) return 0; return 1 << (31 - __builtin_clz(n));}
inline u32 fillbit(u32 n) { if (n == 0) return 0; u32 m = highbit(n); return (m - 1) + m;}
// 应用: 找到大于等于n的最小2整幂u32 find2powgt(u32 n) { // 要求n <= 2^31 return n == 0 ? 1 : fillbit(n - 1) + 1;}求[l, r]所有数字按位与的结果
u32 andsum(u32 l, u32 r) { // [l, r] while (r > l) { r -= lowbit(r); } return r;}二进制低位与高位翻转
神人代码, 但很快. 状压中可能会用上.
inline u32 bitrvs(u32 n) { n = ((n & 0xaaaaaaaa) >> 1) | ((n & 0x55555555) << 1); n = ((n & 0xcccccccc) >> 2) | ((n & 0x33333333) << 2); n = ((n & 0xf0f0f0f0) >> 4) | ((n & 0x0f0f0f0f) << 4); n = ((n & 0xff00ff00) >> 8) | ((n & 0x00ff00ff) << 8); n = (n >> 16) | (n << 16); return n;}二进制中1的数目
// 直接使用内置函数int __builtin_popcount(unsigned int x);int __builtin_popcountll(unsigned long long x);// 注意参数类型(虽然会自动转换).子集枚举
设s是用二进制表示的集合. 以的复杂度枚举s的子集.
for (int i = s; ; i = (i - 1) & s) { // ... if (i == 0) { break; // 处理0, 如果无需处理0的话就移到开头或for语句中. }}遍历所有子集的子集
可以在的时间内遍历大小为n的集合s的子集的所有子集.
for (int m = 0; m < (1 << n); m++) { for (int i = m; i > 0; i = (i - 1) & m) { // ... }}std::bitset
相当于超长二进制整数.
bitset<N> b;b[i] // 访问第 i 位 (0 或 1)b.set(i); // 将第 i 位设为 1b.reset(i);// 将第 i 位设为 0b.flip(i); // 将第 i 位取反 (0变1, 1变0)
b.set(); // 全部设为 1b.reset(); // 全部设为 0b.flip(); // 全部取反
b.count(); // 返回 1 的个数b.any(); // 是否存在 1b.none(); // 是否全为 0
bitset<N> x, y, z;
z = x & y; // 交集z = x | y; // 并集z = x ^ y; // 异或z = ~x; // 取反z = x << 2; // 整体左移 2 位
b._Find_first(); // 返回第一个(最低位)1的下标b._Find_next(idx);// 返回高于idx的位中, 第一个(最低位)1的下标基于模板生成的动态bitset实现
template <int len = 1>void subset_sum(int n) { // 如果当前 bitset 长度 len 不够装下 n (背包容量) if (n >= len) { // 递归调用,尝试 2倍长度,直到达到 maxn subset_sum<std::min(len*2, maxn)>(n); return; }
// 只有当 len > n 时,才会执行下面的代码 // 此时 bitset 的大小是刚刚好大于 n 的 2 的幂次 bitset<len> dp;
dp[0] = 1; for (ll x: b) { dp = dp | (dp << x); } // ... 后续统计答案 ...}以下文本来自gemini3 pro.
AI对此代码的解释为什么这么写?
- 痛点:
std::bitset<N>中的N必须是编译时确定的整数(Constexpr)。你不能写bitset<n> dp(其中n是变量)。- 传统做法:
- 开一个全局最大的
bitset<1000000>,但reset慢。- 手写
vector<u64>模拟(就是我上一条回复写的FastBitset),代码量大。- 此代码的做法: 利用 C++ 模板生成机制。编译器会为你生成一系列函数:
subset_sum<1>(n)subset_sum<2>(n)subset_sum<4>(n)- …
subset_sum<1048576>(n)运行时发生了什么?
假设当前节点子树总大小
n = 50:
- 调用
subset_sum<1>(50)。- 检查
50 >= 1,条件成立 -> 递归调用subset_sum<2>(50)。- … 一路翻倍调用 …
- 调用
subset_sum<64>(50)。- 检查
50 >= 64,条件不成立。- 实例化
bitset<64> dp。- 执行背包 DP。
效果: 对于大小为 50 的子树,它只用了一个 64 位的 bitset,操作和清空都极快! 对于大小为 100000 的子树,它会自动匹配到
bitset<131072>。 这完美实现了“按需分配内存”,且使用的是标准库,无需手写位运算操作!
数据结构: 位图
除了std::bitset外还可以手写bitset, 好处是可以实现范围填充, 以进行时间上的优化(详见P5539 【XR-3】Unknown Mother-Goose).
一般应具有以下操作.
inline int div64(int n) { return n >> 6;}inline int mul64(int n) { return n << 6;}u64 bs[div64(N) + 1];void initbitset(int n);void set(int p);void clear(int p);void flip(int p);bool check(int p); // 查询p位的状态.
// 扩展操作void setall();void clearall();void flipall();void allset();void allclear();int popcnt();void leftshift();void rightshift();void biand(bs a, bs b); // 需要先封装void bior(bs a, bs b);void bixor(bs a, bs b);一些其他的内置函数
int __builtin_clz(unsigned int x); // 求前导0int __builtin_clzll(unsigned long long x);
int __builtin_ctz(unsigned int x); // 求后导0int __builtin_ctzll(unsigned long long x);位运算trick合集
- 从popcount角度看进位: 每个进位均使popcount - 1.
- 求某个集合的子集的异或和: 先按位枚举, 对于每一位来说, 我们把集合分成两类: A集合在这一位上是0, B集合在这一位上是1. 所以只有在B集合中取奇数个, A集合中任意取, 这一位才能是1. 最终这一位是1的子集将会有个.
竞赛算法(3) 位运算
https://fuwari.vercel.app/posts/竞赛算法3-位运算/