本文介绍生成一个集合子集的两种常见算法,借此从中深入理解搜索问题中常见的两种思路。
递归回溯
思路
对于集合中的每个元素,我们都有选择和不选择两种处理方式,这种思路类似于二叉树的遍历,每种情况都向下衍生出两种情况,最终当遍历到下标 index = nums.size()
时,将生成的子集保存。
由于此处我们使用一个数组的引用来保存子集元素,因此在递归回溯时,我们需要手动将上一步中加入添加的元素去除,来回溯到该元素未被选择的状态。
代码
class Solution {
private:
std::vector<std::vector<int>> Sets;
void search(std::vector<int>& subset, std::vector<int>& nums, int index) {
if (index == nums.size()) {
Sets.emplace_back(subset);
return;
}
subset.emplace_back(nums[index]);
search(subset, nums, index + 1);
subset.pop_back();
search(subset, nums, index + 1);
}
public:
std::vector<std::vector<int>> getSubset(std::vector<int>& nums) {
std::vector<int> subset;
search(subset, nums, 0);
return Sets;
}
};
状态压缩
思路
我们知道,一个集合的非空子集个数为 2n - 1,因此可以将子集状态表示为一个范围在 [1, 2n] 的二进制数。
二进制数位 ai 若为 0,表示第 i 位未被选中;若为 1,表示第 i 位被选中。而要分析一个压缩的状态,即获取表示该状态二进制数各位的值,可以运用位运算的操作。由于一个二进制数和 1 进行按位与(&)操作得到的结果将只由该数的最低位决定,如果最低位为 0,则运算结果为 0,否则为 1. 由此可以想到,将一个状态数依次右移 n 位得到一个以 an 结尾的二进制数,再将该二进制数和 1 进行按位与得到 an 的值。依次遍历所有的右移步数得到该状态的所有信息。
代码
class Solution {
private:
std::vector<std::vector<int>> Sets;
public:
std::vector<std::vector<int>> getSubset(std::vector<int>& nums) {
int n = nums.size();
std::vector<int> subset;
for (int i = 1; i <= (1 << n); ++i) {
subset.clear();
for (int j = 0; j < n; ++j) {
int isChoosed = i >> j & 1;
if (isChoosed)
subset.emplace_back(nums[j]);
}
Sets.emplace_back(subset);
}
return Sets;
}
};