079、080、081、082这四道题思路和解法十分相似,但由浅入深,逐步添加了一些干扰设置,使得题目的解法也稍有不同。
剑指 Offer II 079. 所有子集
题目描述
思路
对于每一个元素,都有两个选择:加入子集或者不加入子集:
- 不加入当前子集:继续递归下一个位置;
- 加入当前子集:将当前元素保存到当前子集,递归下一个位置,递归结束后从当前子集中删除该元素;
- 递归边界:当前下标
idx
超过了数组下标范围,保存答案,返回;
时间复杂度 O ( 2 n ) O(2^n) O(2n)
class Solution {
vector<vector<int>> ans;
void dfs(vector<int>& nums, vector<int>& tmp, int idx, int n) {
if (idx == n) {
ans.push_back(tmp);
return;
}
// 当前nums[idx]不加入子集
dfs(nums, tmp, idx + 1, n);
// 当前nums[idx]加入子集
tmp.push_back(nums[idx]);
dfs(nums, tmp, idx + 1, n);
tmp.pop_back();
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector<int> tmp;
dfs(nums, tmp, 0, n);
return ans;
}
};
剑指 Offer II 080. 含有 k 个元素的组合
题目描述:
思路
与079相似,只不过增加了组合中元素的个数限制。
对于每个元素都有选择和不选择两种情况,分别递归进行处理。递归边界是当前的组合中元素数达到了 k ,或者是当前处理的数字num > n
。
class Solution {
vector<vector<int>> ans;
void dfs(vector<int>& arr, int num, int n, int k) {
if (arr.size() == k) {
ans.push_back(arr);
return;
}
if (num > n) {
return;
}
dfs(arr, num + 1, n, k);
arr.push_back(num);
dfs(arr, num + 1, n, k);
arr.pop_back();
}
public:
vector<vector<int>> combine(int n, int k) {
vector<int> arr;
dfs(arr, 1, n, k);
return ans;
}
};
剑指 Offer II 081. 允许重复选择元素的组合
题目描述:
思路
大体思路同之前的题目一样,只不过本题允许一个数字被多次选择。因此在选择当前数字的分支上的处理稍有差异,下次递归要继续使用当前的数字。
另外,当当前数组的累加和已经超过 target
时进行剪枝,减少不必要的递归。
class Solution {
vector<vector<int>> ans;
void dfs(vector<int>& cand, vector<int>& cur, int index, int target, int sum) {
if (index == cand.size()) {
return;
}
if (sum > target) {
return;
}
if (sum == target) {
ans.push_back(cur);
return;
}
dfs(cand, cur, index + 1, target, sum); //不选择当前的数字
cur.push_back(cand[index]);
dfs(cand, cur, index, target, sum + cand[index]); //继续选择当前的数字
cur.pop_back();
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> cur;
dfs(candidates, cur, 0, target, 0);
return ans;
}
};
剑指 Offer II 082. 含有重复元素集合的组合
题目描述
思路
这道题又增加了一种变化,就是数组中可能有重复元素。假如待选数组中有两个1
,一个在前一个在后,那么我选择了前面的1不选择后面的1,和不选择前面的1选择后面的1,这两种情况是相同的。所以要排除掉这种情况。
我们首先将待选数组排序,使得相同的元素靠在一起,当进行“不选择当前数字”的递归分支时,需要跳过后面所有和当前数字相同的数字,这样就避免了上面那种情况的出现。
下面解释一下为什么进行“选择当前数字”这条分支时,我们不能跳过后面相同的数字。假如待选数组中有5个1,即[1,1,1,1,1,3,4,5,...]
,分为下面两种情况:
- 把所有的
1
都选进来,每一个1都应该被选择,所以不能跳过相同的1; - 只选择了部分
1
:以选择了3个1为例,对于这种只选择了部分相同数字的情况,包含在了“选第一个1 --> 选第2个1 --> 选第3个1 --> 不选第4个1(同时也跳过了第5个1)”这种决策里面。所以我们不能跳过相同的1。
另外,我的代码里还有一点需要注意那就是在判断递归边界的时候要先判断if(sum == target)
,不能先判断if (idx > cand.size())
,因为有一种情况是虽然当前 idx 越界了,但是当前数组里的累加和已经为 target ,需要先存储答案再 return。
class Solution {
vector<vector<int>> ans;
void dfs(vector<int>& cand, vector<int>& cur, int idx, int sum, int target) {
if (sum == target) { //先判断sum == target,
ans.push_back(cur);
return;
}
if (sum > target) {
return;
}
if (idx == cand.size()) { //最后判断idx是否越界
return;
}
//不选当前数字,需要跳过后面的相同数字
int k = idx;
while (k < cand.size() && cand[k] == cand[idx]){
k++;
}
dfs(cand, cur, k, sum, target);
//选择当前数字
cur.push_back(cand[idx]);
dfs(cand, cur, idx + 1, sum + cand[idx], target);
cur.pop_back();
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<int> cur;
sort(candidates.begin(), candidates.end());
dfs(candidates, cur, 0, 0, target);
return ans;
}
};