干净又实用的位运算备忘——涵盖位操作的语义、若干常用恒等式与经典技巧(含代码示例和注意事项)。适合刷题/写底层代码/竞赛时速查。
基础语义(按位操作)
&:按位与(AND)。1&1=1,其余为0。用于掩码(mask)/清位。|:按位或(OR)。至少有1则为1。用于置位/合并标志。^:按位异或(XOR)。相同为0,不同为1。用于无进位加法、交换两个数、不重复元素。~:按位取反(NOT)。~x= 按位翻转(注意有符号整型含符号位)。<<:左移(乘2的幂,低位补0)。>>:右移(逻辑/算术取决于语言/类型,注意符号扩展)。-n:负数(在二进制中通常以 二补码 表示),性质:-n = ~n + 1。
二进制与二补码(关键恒等)
- 负数表示(两补):
-n == (~n) + 1。
例:若n = 5 (00000101),则-n = 11111011(8位表示)。 n & (n - 1):清除最低位的 1(把n的最低位1置为0)
证明直观:n-1把最低 1 变 0,并把其右边的 0/1 翻转,因此与操作清掉那个 1。
用途:计数二进制 1 的个数(见下)。n & (-n):提取最低位的 1(最低位 1 的掩码)
例:n = 12 (1100)->n & -n = 0100 (4)。n ^ (n - 1):最低位 1 及其右边全部变成 1(形如...00111...)。常用来构造掩码。~n与-n的组合常用于位运算构造或取反掩码:清第 k 位:n & ~(1<<k)。
经典技巧与代码(C / C++ / Python 表示)
1) 判断是否为 2 的幂
bool isPow2(unsigned n) {
return n && !(n & (n - 1));
}
(零不是幂;n & (n-1) == 0 当且仅当 n 是 2 的幂)
2) 计数二进制 1(Brian Kernighan 算法)
int popcount(unsigned n) {
int cnt = 0;
while (n) {
n &= (n - 1); // 清掉最低位的 1
cnt++;
}
return cnt;
}
3) 提取最低位的 1
unsigned lowbit(unsigned n) {
return n & -n; // 等价于 n & (~n + 1)
}
4) 清掉最低位的 1(得到 n 的下一个子集)
unsigned remove_lowest_one(unsigned n) {
return n & (n - 1);
}
5) 枚举一个集合的所有非空子集(位掩码技巧)
// 遍历 s 的所有子集 t (包括空集,若要排除空集可处理)
for (unsigned t = s; t; t = (t - 1) & s) {
// 处理子集 t
}
6) 交换两个整数(无临时变量,用 XOR)
a ^= b;
b ^= a;
a ^= b;
(仅当 a 与 b 不是同一地址时安全;否则会变 0)
7) 设置 / 清除 / 切换 / 读取第 k 位
// set k-th bit
n |= (1u << k);
// clear k-th bit
n &= ~(1u << k);
// toggle k-th bit
n ^= (1u << k);
// get k-th bit (0/1)
int bit = (n >> k) & 1;
8) 构造从 0 到 k 连续的低位掩码
// mask with lowest k bits set: (1<<k) - 1
unsigned mask = (1u << k) - 1; // 当 k==32 要小心溢出(使用 64 位)
9) 判断两个数是否异号(常用于溢出检测)
bool diff_sign(int x, int y) {
return (x ^ y) < 0;
}
(最高位不同则异号)
10) 向上/向下对齐到 2 的幂(对齐常见)
// 向上对齐 x 到 2^k(假设 x>0 且 k 是幂)
unsigned align_up(unsigned x, unsigned a) {
return (x + a - 1) & ~(a - 1); // a 必须是幂
}
常见题型与位运算解法思路(刷题指南)
- 找出只出现一次/两次/三次的数:使用 XOR / 位按位统计(对每一位计数 mod k)。
- 子集枚举与状态压缩 DP:位掩码遍历、子集转移常用
(s & -s)、s & (s-1)。 - 最低位相关操作:
n & -n、n & (n-1)是常见 O(1) 小技巧。 - 位 DP 中使用
popcount(mask)或预计算__builtin_popcount(GCC/Clang)加速状态代价计算。
注意事项与陷阱
- 有符号右移:
>>对有符号类型通常是算术右移(保持符号),对无符号则做逻辑右移。题目中若依赖位模式,建议使用unsigned。 - 优先级:位运算优先级低于加减、移位高于比较等;用括号避免歧义:
n & (n - 1)。 - 溢出/类型宽度:
1 << 31在 32 位int上可能是 UB/移位边界;尽量使用1u或更宽类型1ULL。 - XOR 交换的局限:当 a 和 b 指向同一内存(例如同一个变量的别名)时会出错;也会降低可读性,现代编译器优化下用临时变量更清晰。
- ~ 运算与负数:
~n与-n都依赖位宽,注意在打印/比较时与有符号/无符号的差别。
小结(记住这几条)
-n = ~n + 1(两补码)n & (n - 1)—— 清最低位 1n & -n—— 提取最低位 1(最低 1 的掩码)n && !(n & (n - 1))—— 判断是否为 2 的幂- 枚举子集:
for (t = s; t; t = (t - 1) & s) - 位运算适合:计数、掩码、状态压缩、加速常数操作
发表回复