位运算在计算机科学中有着广泛的应用,其中利用位运算生成集合的所有子集是一种高效且直观的方法。这种方法的核心在于二进制数与子集选择方案的天然对应关系,而判断某一位是否为 1 的位运算技巧则是实现这一方法的关键。
一、二进制位运算的基础:判断某一位是否为 1
要理解位运算如何生成子集,首先需要掌握如何通过位运算判断一个二进制数中某一位的取值。
1. 1 << i
:构造特定位为 1 的掩码
二进制中,数字1
的原始表示是...0001
(仅最后一位为 1)。左移操作1 << i
会将这个 1 向左移动 i 位,从而构造出一个 “只有第 i 位为 1,其他位均为 0” 的二进制数(称为掩码)。
示例:
-
i=0
时:1 << 0 = 1
→ 二进制...0001
(第 0 位为 1) -
i=1
时:1 << 1 = 2
→ 二进制...0010
(第 1 位为 1) -
i=2
时:1 << 2 = 4
→ 二进制...0100
(第 2 位为 1) -
i=3
时:1 << 3 = 8
→ 二进制...1000
(第 3 位为 1)
这种特性使得1 << i
像一个 “探照灯”,能够精准定位到二进制数的第 i 位。
2. mask & (1 << i)
:判断第 i 位是否为 1
利用 “与运算(&)” 的特性 —— 两个二进制位中只有同时为 1 时结果才为 1,否则为 0—— 可以判断一个数的某一位是否为 1。
结合1 << i
的掩码特性,mask & (1 << i)
的结果由mask
的第 i 位唯一决定:
-
若
mask
的第 i 位为 1:mask
的第 i 位(1)与1 << i
的第 i 位(1)进行与运算,结果为 1(非 0);其他位因1 << i
为 0,运算结果也为 0。整体结果非 0(如mask=5
即0101
,i=2
时,0101 & 0100 = 0100
,结果为 4≠0)。 -
若
mask
的第 i 位为 0:mask
的第 i 位(0)与1 << i
的第 i 位(1)进行与运算,结果为 0;其他位同样为 0。整体结果为 0(如mask=5
即0101
,i=1
时,0101 & 0010 = 0000
,结果为 0)。
因此,mask & (1 << i) ≠ 0
是判断mask
的第 i 位为 1 的充要条件。
二、位运算生成所有子集的原理
子集生成的核心是枚举集合中元素的所有可能选择组合,而位运算能完美匹配这一需求。
1. 子集与二进制数的一一对应
对于包含 n 个元素的数组nums
,每个子集都对应一种元素选择方案(每个元素要么被选中,要么不被选中)。二进制数的每一位恰好能表示这种二元状态:
-
二进制数的第 i 位为 1 → 选中
nums[i]
-
二进制数的第 i 位为 0 → 不选中
nums[i]
因此,每个二进制数(称为 mask)都唯一对应一个子集,反之亦然。
2. 覆盖所有子集的必然性
-
子集总数:n 个元素的子集总数为 2ⁿ(包括空集和全集),因为每个元素有 2 种选择,总方案数为 2×2×…×2(n 次)。
-
二进制数范围:当 mask 从 0 遍历到 2ⁿ-1 时,恰好有 2ⁿ个不同的二进制数,每个数对应唯一的选择方案。例如 n=4 时,mask 从 0(0000)到 15(1111)共 16 个数,对应 16 个子集。
3. 无重复的保证
每个 mask 是唯一的二进制数,其对应的选择方案(哪几位为 1)也是唯一的。例如 mask=3(0011)只能对应 “选择第 0、1 个元素” 的子集,不会与其他 mask 重复。
三、实例演示:生成子集的过程
以nums = [a, b, c, d]
(n=4)为例,mask 与子集的对应关系如下:
子集 | 选择方案(选 = 1,不选 = 0) | mask(二进制) | mask(十进制) |
---|---|---|---|
[] |
a=0, b=0, c=0, d=0 | 0000 | 0 |
[a] |
a=1, b=0, c=0, d=0 | 0001 | 1 |
[b] |
a=0, b=1, c=0, d=0 | 0010 | 2 |
[a, b] |
a=1, b=1, c=0, d=0 | 0011 | 3 |
[c] |
a=0, b=0, c=1, d=0 | 0100 | 4 |
[a, c] |
a=1, b=0, c=1, d=0 | 0101 | 5 |
[b, c] |
a=0, b=1, c=1, d=0 | 0110 | 6 |
[a, b, c] |
a=1, b=1, c=1, d=0 | 0111 | 7 |
[d] |
a=0, b=0, c=0, d=1 | 1000 | 8 |
… | … | … | … |
[a, b, c, d] |
a=1, b=1, c=1, d=1 | 1111 | 15 |
四、代码实现示例
利用上述原理,可写出生成所有子集的代码:
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& vec) {
os << "[";
for (size_t i = 0; i < vec.size(); i++) {
os << vec[i];
if (i != vec.size() - 1) os << ",";
}
os << "]";
return os;
}
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
for(int mask = 0;mask<(1<<n);++mask){
vector<int> sig;
for(int j =0;j<n;j++){
if(mask & (1<<j)) {
std::cout<<"J:" <<j<<std::endl;
sig.push_back(nums[j]);}
}
res.push_back(sig);
std::cout<< "mask:" <<mask<< " sig" <<sig<<std::endl;
}
return res;
}
};
int main(){
std::vector<int> nums = {0, 1, 2, 3, 5, 1, 0, 1};
Solution solu;
solu.subsets(nums);
}
总结
位运算生成子集的本质是:用二进制数的每一位对应元素的 “选 / 不选” 状态,通过遍历所有可能的二进制数(0 到 2ⁿ-1),结合mask & (1 << i)
判断位值的技巧,实现对所有子集的枚举。这种方法既保证了不重复、不遗漏,又具有高效、代码简洁的特点,是处理子集生成问题的经典方案。