位运算

位运算不要背零散技巧,先抓住“位掩码就是一个超轻量状态数组”这个本质,再去套判断、压缩、分组和枚举四类问题。

核心思路

位运算题常见成分只有四种:

  1. 用 & 判断某一位是不是 1。
  2. 用 | 或 ^ 修改状态。
  3. 用 n & (n - 1)、n & -n 提取最低位信息。
  4. 用 mask 压缩一个集合或一个访问状态。

很多题看上去花哨,本质上都在回答两个问题:

  1. 我能不能用一个整数表示一组状态。
  2. 我能不能利用二进制性质把原来的 O(n) 判断压到 O(1)。

先记住的几个不变量

  • n & 1 可以判断奇偶。
  • n & (n - 1) 会删掉最低位的一个 1。
  • n & -n 会保留最低位的一个 1。
  • 如果一个正整数是 2 的幂,那么它的二进制里只会有一个 1。
  • a ^ a = 0,a ^ 0 = a,所以异或特别适合“成对抵消”。

常用 Go 模板

基础位操作

func getBit(x, i int) int {
	return (x >> i) & 1
}
 
func setBit(x, i int) int {
	return x | (1 << i)
}
 
func clearBit(x, i int) int {
	return x & ^(1 << i)
}
 
func isPowerOfTwo(n int) bool {
	return n > 0 && (n&(n-1)) == 0
}
 
func countOnes(n int) int {
	cnt := 0
	for n != 0 {
		n &= n - 1
		cnt++
	}
	return cnt
}
 
func lowbit(n int) int {
	return n & -n
}

经典题:只出现一次的数字

func singleNumber(nums []int) int {
	res := 0
	for _, x := range nums {
		res ^= x
	}
	return res
}

这里的核心不是“异或很神奇”,而是“相同元素会两两抵消”。

位掩码枚举子集

func subsets(nums []int) [][]int {
	n := len(nums)
	res := make([][]int, 0, 1<<n)
 
	for mask := 0; mask < (1 << n); mask++ {
		path := make([]int, 0, n)
		for i := 0; i < n; i++ {
			if (mask & (1 << i)) != 0 {
				path = append(path, nums[i])
			}
		}
		res = append(res, path)
	}
 
	return res
}

这个模板后面可以直接迁移到 状态压缩DP、TSP、集合搜索等题型。

常见题型

判断与构造

异或分组

状态压缩

易错点

位运算真正容易错的地方,不是公式本身,而是数据范围、符号位和语言细节。

  • Go 里 ^ 既是按位异或,也是单目按位取反,读代码时要分清上下文。
  • 负数右移和补码细节容易出问题,做题时优先先把题目里的数据范围想清楚。
  • 1 << n 在 n 很大时可能溢出,状态压缩通常只适合 n <= 20 左右的题。
  • 用位掩码表示集合时,先约定“第 i 位代表什么”,否则后面很容易写乱。

学习顺序

  1. 先刷判断类题,熟悉 &、|、^、移位。
  2. 再刷异或类题,理解“抵消”和“分组”。
  3. 最后做状态压缩,把位运算和 回溯、动态规划 串起来。

相关主题


返回:算法学习导航