问题一:假设一个整数集合中不包含重复元素,求这个集合的所有子集(包括空集和本身)
一、子集就是在当前集合中选择N(N>=0)个元素,重新构成的另一个集合。那么对于当前元素我都可以选或者不选,重复这个子过程,那么子集便选择好了,很容易想到递归。
问题二:假设整数集合中包含重复元素,求这个集合的所有子集(包括空集和本身),子集互不相同
问题三:从集合中选择包含指定元素数量(k)的子集
一、子集就是在当前集合中选择N(N>=0)个元素,重新构成的另一个集合。那么对于当前元素我都可以选或者不选,重复这个子过程,那么子集便选择好了,很容易想到递归。
void subsetsRecursiveCore(vector<int>& S, size_t index, vector<int>& item, vector<vector<int> >& ret)
{
//本质上是DFS,对于S中的每一个元素都做一个选择,选或者不选,当index==S.size()说明所有元素判断完毕,获得一个子集
if (index == S.size())
{
ret.push_back(item);
return;
}
subsetsRecursiveCore(S, index + 1, item, ret);//不选当前元素,直接判断下一个元素
item.push_back(S[index]);//选择当前元素
subsetsRecursiveCore(S, index + 1, item, ret);
item.pop_back();//回溯,在路径上删除该元素
}
void subsetsRecursive(vector<int>& S, vector<vector<int> >& subsets)
{
vector<int> item;
subsetsRecursiveCore(S, 0, item, subsets);
}
二、将每个集合映射为一个整数,整数第k个二进制位为1表示选择当前集合的第k个元素构成子集,例如S={A, B, C, D},则0110=6表示子集{B, C}。这种方法的巧妙之处是它不仅能生成子集,还能方便地表示集合的并、交、差等集合运算。位运算的或、与、异或对应着集合的并、交、对称差。当然,这种方法的限制是集合元素个数不能超过整数的位数。
void subsetsBitwise(vector<int>& S, vector<vector<int> >& subsets)
{
const size_t n = S.size();
const size_t max = 1 << n;
vector<int> v;
v.reserve(n);
for (size_t i = 0; i < max; ++i) //无符号数i中0变到最大,然后将其转化为集合
{
for (size_t j = 0; j < n; ++j)
{
if (i & 1 << j) v.push_back(S[j]);//i中哪一位是1则说明选择对应的元素
}
subsets.push_back(v);
v.clear();
}
}
三、在已有的子集中添加元素构成新的子集,subset(n) = join(subset(n-1), (subset(n-1), S[n])),每考虑一个新的元素,就是把这个元素与之前得到的结果都结合一次,再并进之前的结果,得出新结果。
void subsetsDP(vector<int>& S, vector<vector<int> >& subsets)
{
subsets.resize(1);//初始化subsets中只包含一个空集
for (size_t i = 0; i < S.size(); ++i)
{
size_t j = subsets.size();
while (j--)
{
subsets.push_back(subsets[j]);//先拷贝已存在的所有集合,然后再在新加集合中添加当前元素
subsets.back().push_back(S[i]);
}
}
}
问题二:假设整数集合中包含重复元素,求这个集合的所有子集(包括空集和本身),子集互不相同
如果当前元素与上一个元素是重复的,那么当前元素与比上一个元素还要早产生的子集结合是一定重复的,因为上一个元素已经经历了相同的过程了,因此当前元素只能跟上一个元素产生的新子集进行结合才能保证不重复。 于是思路就有了,就是遇到有重复现象时,就不是与之前的所有子集结合,而只与上一个元素产生的新子集进行结合,这里实现就需要记录上一个元素产生子集的起始结束
void subsetsDP2(vector<int>& S, vector<vector<int> >& subsets)
{
std::sort(S.begin(), S.end());//使得重复元素相邻
subsets.resize(1);//初始化subsets中只包含一个空集
size_t prevPos = 0;
int prevValue = 0;
for (size_t i = 0; i < S.size(); ++i)
{
size_t subsetSize = subsets.size();
if (S[i] != prevValue || i == 0)
{
prevPos = 0;
}
//当前元素与上一个元素相同,只在上次新加的集合中追加当前元素
for (size_t j = prevPos; j < subsetSize; ++j)
{
subsets.push_back(subsets[j]);//先拷贝已存在的所有集合,然后再在新加集合中添加当前元素
subsets.back().push_back(S[i]);
}
prevPos = subsetSize;
prevValue = S[i];
}
}
采用递归的方式也可以解决,代码更加简单
void subsets_recursive1(const vector<int>& nums, int start, vector<int>& tmp, vector<vector<int>>& result)
{
for (int i = start; i < nums.size(); ++i)
{
if (i > start && nums[i] == nums[i-1]) continue; //如果nums[i]与nums[i-1]相等,那么就是经历同样的递归过程
tmp.push_back(nums[i]);
subsets(nums, i + 1, tmp, result);
tmp.pop_back();
}
result.push_back(tmp);
}
问题三:从集合中选择包含指定元素数量(k)的子集
同样可以采用递归的方法,但是需要注意的是递归终止条件不仅仅是已选择的元素个数达到k,还有就是数组中剩下的可选元素的个数小于k
//从长度为n的数组中选择k个数
void select_k(const vector<int>& nums, int k, int start, vector<int>& tmp, vector<vector<int>>& result)
{
if (k == 0)//已经选择k个元素了
{
result.push_back(tmp);
return;
}
for (int i = start; i <= nums.size() - k; ++i) //如果数组中的数少于k个,提前结束递归
{
if (i > start && nums[i] == nums[i-1]) continue;
tmp.push_back(nums[i]);
select_k(nums, k - 1, i + 1, tmp, result);
tmp.pop_back();
}
}