1180 字
6 分钟
竞赛算法(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的下标数据结构: 位图
除了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-位运算/