二进制枚举算法
二进制枚举算法详解
一、什么是二进制枚举算法
二进制枚举算法是一种 通过整数的二进制表示来枚举所有子集 的方法。适用于处理组合问题,特别是与“取或不取”某个元素相关的场景。
📌 原理说明
设有一个集合 A = [a₀, a₁, ..., aₙ₋₁]
,我们要枚举它的所有子集。集合中每个元素有两种状态:选 or 不选。因此总共有 2ⁿ
种子集。
我们可以用一个 n
位的 二进制数 来表示一个子集,例如:
二进制位 | 子集含义 |
---|---|
0 | 不选该元素 |
1 | 选该元素 |
比如对于集合 [1, 2, 3]
:
000
→ 空集{}
101
→ 选第1和第3个元素 →{1, 3}
111
→ 全选 →{1, 2, 3}
✅ 实现步骤
- 确定元素个数:假设为
n
- 循环从
0
到2ⁿ - 1
,每个数的二进制表示对应一个子集 - 用位运算判断每一位是否为
1
- 构造当前子集
- 输出或处理这个子集
二、图示理解
假设集合为 [a, b, c]
,其子集枚举如下(每一行是一个整数的二进制表示):
css复制编辑i(二进制) 是否选a 是否选b 是否选c 子集
000 0 0 0 {}
001 0 0 1 {c}
010 0 1 0 {b}
011 0 1 1 {b, c}
100 1 0 0 {a}
101 1 0 1 {a, c}
110 1 1 0 {a, b}
111 1 1 1 {a, b, c}
三、代码实现(Go 示例)
✅ Go 实现:
go复制编辑package mainimport "fmt"func binaryEnum(arr []int) {n := len(arr)for i := 0; i < (1 << n); i++ {var subset []intfor j := 0; j < n; j++ {if (i>>j)&1 == 1 {subset = append(subset, arr[j])}}fmt.Println(subset)}
}func main() {arr := []int{1, 2, 3}binaryEnum(arr)
}
四、二进制枚举的优势
优势 | 说明 |
---|---|
✅ 简洁 | 使用整数和位运算代替递归或多重循环 |
✅ 效率高 | 位运算在底层执行非常快 |
✅ 结构清晰 | 子集状态一一对应二进制 |
✅ 易扩展 | 可结合子集统计、位掩码优化 |
五、适用场景
✅ 小规模子集枚举问题
当元素个数不大(通常 n ≤ 20
),可暴力枚举 2ⁿ
种组合。
✅ 集合操作(求交并差)
集合可用一个二进制数表示,如 set = 0b10101
表示选了第1、第3、第5个元素。
✅ 状态压缩动态规划(Bitmask DP)
如 TSP(旅行商问题)、集合覆盖、最短Hamilton路径等问题。
✅ 穷举判断满足某种条件的子集
如:求所有子集中和为某个目标值的个数。
六、进阶优化技巧
- 提前剪枝:遇到明显不符合要求的中间状态时立即跳过
- 记录中间状态:用 map 缓存某些子集结果
- 结合排列枚举:在每个子集内部再对元素进行排列
七、常见误区
误区 | 正确做法 |
---|---|
n 很大时也使用二进制枚举 | 超过 n = 25 后就非常慢,应换动态规划或回溯 |
忘记位移方向 | (i >> j) 是向右移第 j 位 |
忽视索引越界 | 注意 arr[j] 不要超过范围 |
忽略子集顺序 | 二进制枚举默认顺序由低到高 |
八、小练习建议
- 给定整数数组,输出所有子集
- 求所有子集中元素和为
target
的子集个数 - 二进制枚举所有满足异或为 0 的子集
- 用二进制枚举解决集合划分问题(如背包)