Subsets & Combinations & Combination Sum
Backtracking 不变的经典啊。
唯一注意的地方是,start == nums.length 的时候其实也是要添加结果的,不然会错。写回溯的时候,base case 要想清楚。
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> rst = new ArrayList<>();
dfs(rst, new ArrayList<>(), nums, 0);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int[] nums, int start){
//if(start > nums.length) return;
rst.add(new ArrayList<Integer>(list));
for(int i = start; i < nums.length; i++){
list.add(nums[i]);
dfs(rst, list, nums, i + 1);
list.remove(list.size() - 1);
}
}
}
有重复元素也不难,先 sort,然后每一步上注意同样的元素别重复加一遍就好了。
public class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> rst = new ArrayList<>();
Arrays.sort(nums);
dfs(rst, new ArrayList<>(), nums, 0);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int[] nums, int start){
rst.add(new ArrayList<Integer>(list));
for(int i = start; i < nums.length; i++){
list.add(nums[i]);
dfs(rst, list, nums, i + 1);
list.remove(list.size() - 1);
while(i + 1 < nums.length && nums[i] == nums[i + 1]) i++;
}
}
}
这类题的搜索树都是极度向左倾斜的结构,节点数为 2^n;
public class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> rst = new ArrayList<>();
dfs(rst, new ArrayList<Integer>(), n, k, 1);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int n, int k, int cur){
if(k == 0){
rst.add(new ArrayList<>(list));
return;
}
for(int i = cur; i <= n; i++){
list.add(i);
dfs(rst, list, n, k - 1, i + 1);
list.remove(list.size() - 1);
}
}
}
为什么觉得我好像在这 gitbook 里写过这题。。
元素可以重复选用,传进去的 index 可以以 i 为准。
但是还是不能走回头路,不然会有重复答案。
所有元素为正整数非常重要,要么这题可能有无数个解。
public class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> rst = new ArrayList<>();
dfs(rst, new ArrayList<>(), candidates, target, 0, 0);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int[] nums, int target, int sum, int start){
if(sum > target) return;
if(sum == target){
rst.add(new ArrayList<Integer>(list));
return;
}
for(int i = start; i < nums.length; i++){
list.add(nums[i]);
dfs(rst, list, nums, target, sum + nums[i], i);
list.remove(list.size() - 1);
}
}
}
没重复元素,想避免重复解,用 index 单调向前;
有重复元素,想避免重复解,index 之外每轮 dfs 末尾直接把指针移到下一个新元素上,一个元素在一轮 for loop 里只加一次。
public class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> rst = new ArrayList<>();
Arrays.sort(candidates);
dfs(rst, new ArrayList<>(), candidates, target, 0, 0);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int[] nums, int target, int sum, int start){
if(sum > target) return;
if(sum == target){
rst.add(new ArrayList<Integer>(list));
return;
}
for(int i = start; i < nums.length; i++){
list.add(nums[i]);
dfs(rst, list, nums, target, sum + nums[i], i + 1);
list.remove(list.size() - 1);
while(i + 1 < nums.length && nums[i] == nums[i + 1]) i++;
}
}
}
Trivial problem.
public class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> rst = new ArrayList<>();
dfs(rst, new ArrayList<>(), n, k, 0, 1);
return rst;
}
private void dfs(List<List<Integer>> rst, List<Integer> list, int target, int k, int sum, int start){
if(sum > target || k < 0) return;
if(sum == target && k == 0){
rst.add(new ArrayList<Integer>(list));
return;
}
for(int i = start; i <= 9; i++){
list.add(i);
dfs(rst, list, target, k - 1, sum + i, i + 1);
list.remove(list.size() - 1);
}
}
}
题目换成这样了之后依然是一个搜索问题,但是有了新的有趣特性。
主要在于这题和之前的 combination sum 还不太一样,它把 [1,3] 和 [3,1] 这种重复搜索算成两个解。于是强行制造了 overlap subproblems.
于是这题用搜索解会 TLE,加个 hashmap 做记忆化搜索,立刻就过了。
24ms ,时间复杂度较高,不如下面那个迭代的写法速度快。
O(n^d),d代表得到 >= target 的搜索深度。
public class Solution {
public int combinationSum4(int[] nums, int target) {
return dfs(nums, 0, target, new HashMap<Integer, Integer>());
}
private int dfs(int[] nums, int curSum, int target, HashMap<Integer, Integer> map){
if(curSum > target) return 0;
if(curSum == target) return 1;
if(map.containsKey(curSum)) return map.get(curSum);
int count = 0;
for(int i = 0; i < nums.length; i++){
count += dfs(nums, curSum + nums[i], target, map);
}
map.put(curSum, count);
return count;
}
}
论坛里还有比较有趣的迭代写法,只需要 3ms,原理和 climbing stairs 差不多,建一个大小等于 target + 1 的 array 代表多少种不同的方式跳上来,依次遍历即可。
时间复杂度 O(n * target) + O(n log n)
public class Solution {
public int combinationSum4(int[] nums, int target) {
Arrays.sort(nums);
int[] res = new int[target + 1];
for (int i = 1; i < res.length; i++) {
for (int num : nums) {
if (num > i)
break;
else if (num == i)
res[i] += 1;
else
res[i] += res[i-num];
}
}
return res[target];
}
}
原理和 climbing stairs 差不多,建一个大小等于 target + 1 的 array 代表多少种不同的方式跳上来,依次遍历即可。
时间复杂度 O(n * target)
注意这里内外循环的顺序不能错,要先按 sum 从小到大的顺序看,然后才是遍历每个元素。因为所谓 bottom-up,是相对于 sum 而言的,不是相对于已有元素的 index 而言的。
可以看到,在只取 min/max 的时候我们不同的循环顺序都能保证正确答案;但是求解的数量时,要以 target value 为外循环。
public class Solution {
public int combinationSum4(int[] nums, int target) {
// dp[sum] = number of ways to get sum
int[] dp = new int[target + 1];
// initialize, one way to get 0 sum with 0 coins
dp[0] = 1;
for(int j = 1; j <= target; j++){
for(int i = 0; i < nums.length; i++){
if(j - nums[i] >= 0) dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
}
Last updated
Was this helpful?