1180 字
6 分钟
竞赛算法(3) 位运算
2025-11-24
无标签

简单的跳过, 从典型trick开始.

lowbit#

inline u32 lowbit(u32 x) {
return x & (-x);
}

分类求缺失数#

数组中只有2种数出现次数为奇数, 其他数出现次数均为偶数, 找到出现次数为奇数的数.

  1. 设这两个数为a和b.
  2. 取异或和sum, 则显然sum = a ^ b.
  3. sum中至少存在一个1, 我们取任意一个1. 这里方便起见取一定存在的lowbit(sum).
  4. lowbit(sum)一定由0和1异或而成, 所以a和b在lowbit(sum)这位上必然一个为0一个为1.
  5. 我们将所有数分类成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是用二进制表示的集合. 以O(2popcnt(n))O(2^{popcnt(n)})的复杂度枚举s的子集.

for (int i = s; ; i = (i - 1) & s) {
// ...
if (i == 0) {
break;
// 处理0, 如果无需处理0的话就移到开头或for语句中.
}
}

遍历所有子集的子集#

可以在O(3n)O(3^n)的时间内遍历大小为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 位设为 1
b.reset(i);// 将第 i 位设为 0
b.flip(i); // 将第 i 位取反 (0变1, 1变0)
b.set(); // 全部设为 1
b.reset(); // 全部设为 0
b.flip(); // 全部取反
b.count(); // 返回 1 的个数
b.any(); // 是否存在 1
b.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); // 求前导0
int __builtin_clzll(unsigned long long x);
int __builtin_ctz(unsigned int x); // 求后导0
int __builtin_ctzll(unsigned long long x);

位运算trick合集#

  1. 从popcount角度看进位: 每个进位均使popcount - 1.
  2. 求某个集合的子集的异或和: 先按位枚举, 对于每一位来说, 我们把集合分成两类: A集合在这一位上是0, B集合在这一位上是1. 所以只有在B集合中取奇数个, A集合中任意取, 这一位才能是1. 最终这一位是1的子集将会有2n12^{n - 1}个.
竞赛算法(3) 位运算
https://fuwari.vercel.app/posts/竞赛算法3-位运算/
作者
ykindred
发布于
2025-11-24
许可协议
CC BY-NC-SA 4.0