所有子集
给定一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
分析:
递归实现。定义函数 dfs ( cur , n ) \text{dfs}(\textit{cur}, n) dfs(cur,n) 参数表示当前位置是 cur \textit{cur} cur,原序列总长度为 n。原序列的每个位置在答案序列中的状态有被选中和不被选中两种,用 t 数组存放已经被选出的数字。在进入 dfs ( cur , n ) \text{dfs}(\textit{cur}, n) dfs(cur,n) 之前 [ 0 , cur − 1 ] [0, \textit{cur} - 1] [0,cur−1]位置的状态是确定的,而 [ cur , n − 1 ] [\textit{cur}, n - 1] [cur,n−1] 内位置的状态是不确定的, dfs ( cur , n ) \text{dfs}(\textit{cur}, n) dfs(cur,n) 需要确定 cur 位置的状态,然后求解子问题 dfs ( c u r + 1 , n ) {\text{dfs}(cur + 1}, n) dfs(cur+1,n)。对于 cur 位置,我们需要考虑a[cur] 取或者不取,如果取,我们需要把 a[cur] 放入一个临时的答案数组中(即上面的 t),再执行 dfs ( c u r + 1 , n ) {\text{dfs}(cur + 1}, n) dfs(cur+1,n),执行结束后需要对 t 进行回溯;如果不取,则直接执行 dfs(cur+1,n)。在整个递归调用的过程中,cur 是从小到大递增的,当 cur 增加到 n 的时候,记录答案并终止递归。
class Solution {
List<Integer> list = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
dfs(0, nums);
return res;
}
public void dfs(int cur, int[] nums){
if (cur == nums.length){
res.add(new ArrayList<>(list));
return;
}
list.add(nums[cur]);
dfs(cur+1, nums);
list.remove(list.size()-1);
dfs(cur+1, nums);
}
}
含有K个元素的组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
分析:
与上一题类似,使用递归来解决。将上一个题的函数dfs中增加一个参数k,即 dfs ( cur , n , k ) \text{dfs}(\textit{cur}, n, k) dfs(cur,n,k),如果当前数组的长度==k则把数组添加到答案中去,同时增加剪枝代码:如果当前数组长度+剩余长度(n-cur+1) < k的话直接舍弃掉下面的递归。
class Solution {
List<Integer> list = new ArrayList<>();
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if (k>n) return null;
dfs(1,n,k);
return ans;
}
public void dfs(int cur, int n, int k){
if (list.size() == k){
ans.add(new ArrayList<>(list));
return;
}
if (list.size()+(n-cur+1)<k) return;
list.add(cur);
dfs(cur+1, n ,k);
list.remove(list.size()-1);
dfs(cur+1,n,k);
}
}
允许重复选择元素的组合
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
分析:
dfs+剪枝+回溯。首先顺序的选择candidates的数字,对于当前的数字有两种选择:1. 选, 下次依旧针对这个数选或不选;2. 不选, 直接跳过该数字,后面不选这个数。
class Solution {
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates, target, 0, new ArrayList<Integer>());
return result;
}
public void dfs(int[] candidates, int target, int start, ArrayList<Integer> list) {
if (target == 0) {
result.add(new ArrayList(list));
return;
}
if (target < 0 || start >= candidates.length) return;
//对于当前数字candidates[start]
//选
list.add(candidates[start]);
dfs(candidates, target-candidates[start], start, list);
list.remove(list.size()-1);
//或者不选
dfs(candidates, target, start+1, list);
}
}
含有重复元素集合的组合
给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。
分析:
定义函数dfs(idx,target):选中candidate[idx],同时与从下标为[idx + 1, candidate.length)中选取元素一起构成和为target的所有不重复组合的集合。为了避免重复的答案,首先要做的就是给数组排序,如果说在同一级递归中,遇到两个相同的数,应该只dfs靠前的那一个一次。原因可以这样理解,如果现在遇到下标位idx,idx + 1的两个数是相同的,那么对于集合dfs(idx, target) 和 dfs(idx + 1, target),后者就是前者的一个子集,所以在同一级递归中,对于相同的数,只应该dfs一次,并且是下标最小的那一个。
剪枝就是基于很直接的思想,例如:前面已经给数组排序了,如果递归的过程中当前值比target大,那么说明后面的值不可能再找出一组元素和为target,所以此时就可以立即结束递归返回。
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
int n = candidates.length;
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(candidates);
dfs(candidates, n, 0, target, new ArrayList<>(), ans);
return ans;
}
public void dfs(int[] candidate, int n, int idx, int target, List<Integer> list, List<List<Integer>> ans) {
if (target == 0) {
ans.add(new ArrayList<>(list));
return ;
}
for (int i = idx; i < n; i++) {
if (candidate[i] > target) { // 剪枝
break;
}
if (i > idx && candidate[i] == candidate[i - 1]) { // 剪枝、避免重复
// 因为前面那个同样的数已经经历过dfs,再拿同样的数dfs就会得到重复的答案
continue;
}
list.add(candidate[i]);
dfs(candidate, n, i + 1, target - candidate[i], list, ans);
list.remove(list.size() - 1);
}
}
}