leetcode第39题

简介: 对回溯法又有了更深的了解,一般的架构就是一个大的 for 循环,然后先 add,接着利用递归进行向前遍历,然后再 remove ,继续循环。而解法二的动态规划就是一定要找到递进的规则,开始的时候就想偏了,导致迟迟想不出来。

image.png

给几个数字,一个目标值,输出所有和等于目标值的组合。

解法一 回溯法

参考这里) ,就是先向前列举所有情况,得到一个解或者走不通的时候就回溯。和37题有异曲同工之处,也算是回溯法很典型的应用,直接看代码吧。

publicList<List<Integer>>combinationSum(int[] nums, inttarget) {
List<List<Integer>>list=newArrayList<>();
backtrack(list, newArrayList<>(), nums, target, 0);
returnlist;
}
privatevoidbacktrack(List<List<Integer>>list, List<Integer>tempList, int [] nums, intremain, intstart){
if(remain<0) return;
elseif(remain==0) list.add(newArrayList<>(tempList));
else{ 
for(inti=start; i<nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain-nums[i], i); 
//找到了一个解或者 remain < 0 了,将当前数字移除,然后继续尝试tempList.remove(tempList.size() -1);
        }
    }
}

时间复杂度:

空间复杂度:

解法二 动态规划

参考这里。动态规划的关键就是找到递进关系,看到了下边的评论想通的。

image.png我们用一个 opt 的 list,然后依次求出 opt [ 0 ],opt [ 1 ] ... opt [ target ]。

opt[0],表示和为 0 的所有情况的组合。

opt[1],表示和为 1 的所有情况的组合。

opt[2],表示和为 2 的所有情况的组合。

...

opt[target],表示和为 target 的所有情况的组合,也就是题目所要求的。

递进关系就是,sum 代表要求的和,如果想求 opt [ sum ] ,就遍历给定的数组 nums,然后分两种情况。

  • 如果 sum 刚好等于 nums [ i ],那么就直接把 nums [ i ] 加到 list 里,算作一种情况。
    例如 nums = [ 2, 3, 6, 7 ] , target = 7。
    当求 sum = 3 的时候,也就是求 opt [ 3 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] == sum == 3,所以此时 opt [ 3 ] = [ [ 3 ] ]。
  • 如果 sum 大于 nums [ i ],那么我们就把 opt [ sum - nums [ i ] ] 的所有情况都加上 nums [ i ] 然后作为 opt [ sum ] 。
    例如 nums = [ 1, 2, 3, 6, 7 ] , target = 7。
    当 sum 等于 5 的时候,也就是求 opt [ 5 ] 的时候,此时当遍历到 nums [ 1 ],此时 nums [ 1 ] = 2 < sum。然后,就看 opt [ sum - nums [ i ] ] = opt [ 5 - 2 ] = opt [ 3 ],而 opt [ 3 ] 在之前已经求好了,opt [ 3 ] = [ [ 1, 2 ], [ 3 ] ],然后把 opt [ 3 ] 中的每一种情况都加上 nums [ 1 ] ,也就是 2,就变成了 [ [ 1, 2, 2 ], [ 3, 2 ] ],这个就是遍历到 nums [ 1 ] 时候的 opt [ 5 ]了。

上边的想法看起来没什么问题,但跑了下遇到一个问题。

比如求 nums = [ 2, 3, 6, 7 ] , target = 7 的时候。

求 opt [ 5 ],然后遍历到 nums [ 0 ] = 2 的时候,就把 opt [ 3 ] = [ [ 3 ] ] 的所有情况加上 2,也就是[ 3 2 ] 加到 opt [ 5 ] 上。接着遍历到 nums [ 2 ] = 3 的时候,就把 opt [ 2 ] = [ [ 2 ] ] 的所有情况加上 3,然后 [ 2 3 ] 这种情况加到 opt [ 5 ] 上,此时 opt [ 5 ] = [ [ 3 2],[ 2 3 ] ]。这样出现了重复的情况,需要解决一下。

这样就相当于二维数组去重,也就是 [ [ 3 2 ],[ 2 3 ] , [ 1 ] ] 这样的列表去重变成 [ [ 2 3 ] , [ 1 ] ] 。最普通的想法就是两个 for 循环然后一个一个比对,把重复的去掉。但这样实在是太麻烦了,因为比对的时候又要比对列表是否相等,比对列表是否相等又比较麻烦。

这里看到一个方法,就是把每个 list 转成 string,然后利用 HashMap 的 key 是唯一的,把每个 list 当做 key 加入到 HashMap 中,这样就实现了去重,然后再把 string 还原为 list。

privateList<List<Integer>>removeDuplicate(List<List<Integer>>list) {
Map<String, String>ans=newHashMap<String, String>();
for (inti=0; i<list.size(); i++) {
List<Integer>l=list.get(i);
Collections.sort(l);
Stringkey="";
//[ 2 3 4] 转为 "2,3,4"for (intj=0; j<l.size() -1; j++) {
key=key+l.get(j) +",";
        }
key=key+l.get(l.size() -1);
ans.put(key, "");
    }
//根据逗号还原 ListList<List<Integer>>ans_list=newArrayList<List<Integer>>();
for (Stringk : ans.keySet()) {
String[] l=k.split(",");
List<Integer>temp=newArrayList<Integer>();
for (inti=0; i<l.length; i++) {
intc=Integer.parseInt(l[i]);
temp.add(c);
        }
ans_list.add(temp);
    }
returnans_list;
}

然后结合去重的方法,我们的问题就解决了。

publicList<List<Integer>>combinationSum(int[] nums, inttarget) {
List<List<List<Integer>>>ans=newArrayList<>(); //opt 数组Arrays.sort(nums);// 将数组有序,这样可以提现结束循环for (intsum=0; sum<=target; sum++) { // 从 0 到 target 求出每一个 optList<List<Integer>>ans_sum=newArrayList<List<Integer>>();
for (inti=0; i<nums.length; i++) { //遍历 numsif (nums[i] ==sum) { 
List<Integer>temp=newArrayList<Integer>();
temp.add(nums[i]);
ans_sum.add(temp);
            } elseif (nums[i] <sum) {
List<List<Integer>>ans_sub=ans.get(sum-nums[i]);
//每一个加上 nums[i]for (intj=0; j<ans_sub.size(); j++) {
List<Integer>temp=newArrayList<Integer>(ans_sub.get(j));
temp.add(nums[i]);
ans_sum.add(temp);
                }
            } else {
break;
            }
        }
ans.add(sum, ans_sum);
    }
returnremoveDuplicate(ans.get(target));
} 
privateList<List<Integer>>removeDuplicate(List<List<Integer>>list) {
Map<String, String>ans=newHashMap<String, String>();
for (inti=0; i<list.size(); i++) {
List<Integer>l=list.get(i);
Collections.sort(l);
Stringkey="";
//[ 2 3 4 ] 转为 "2,3,4"for (intj=0; j<l.size() -1; j++) {
key=key+l.get(j) +",";
        }
key=key+l.get(l.size() -1);
ans.put(key, "");
    }
//根据逗号还原 ListList<List<Integer>>ans_list=newArrayList<List<Integer>>();
for (Stringk : ans.keySet()) {
String[] l=k.split(",");
List<Integer>temp=newArrayList<Integer>();
for (inti=0; i<l.length; i++) {
intc=Integer.parseInt(l[i]);
temp.add(c);
        }
ans_list.add(temp);
    }
returnans_list;
}

时间复杂度:

空间复杂度:

还有另一种思路可以解决重复的问题。

之前对于 nums = [ 2, 3, 6, 7 ] , target = 7 ,我们用了两层 for 循环,分别对 opt 和 nums 进行遍历。

我们先求 opt [ 0 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]

然后再求 opt [ 1 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]

然后再求 opt [ 2 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]

...

最后再求 opt [ 7 ],通过遍历 nums [ 0 ], nums [ 1 ], nums [ 2 ], nums [ 3 ]。

求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况。

我们可以把两个 for 循环的遍历颠倒一下,外层遍历 nums,内层遍历 opt。

考虑 nums [ 0 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。

考虑 nums [ 1 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。

考虑 nums [ 2 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。

考虑 nums [ 3 ],求出 opt [ 0 ],求出 opt [ 1 ],求出 opt [ 2 ],求出 opt [ 3 ] ... 求出 opt [ 7 ]。

这样的话,每次循环会更新一次 opt [ 7 ],最后次更新的 opt [ 7 ] 就是我们想要的了。

这样之前的问题,求 opt [ 5 ] 的时候,出现了 [ 2 3 ],[ 3 2 ] 这样重复的情况就不会出现了,因为当考虑 nums [ 2 ] 的时候,opt [ 3 ] 里边还没有加入 [ 3 ] 。

思路就是上边说的了,但是写代码的时候遇到不少坑,大家也可以先尝试写一下。

publicList<List<Integer>>combinationSum(int[] nums, inttarget) {
List<List<List<Integer>>>ans=newArrayList<>();
Arrays.sort(nums);
if (nums[0] >target) {
returnnewArrayList<List<Integer>>();
    }
// 先初始化 ans[0] 到 ans[target],因为每次循环是更新 ans,会用到 ans.get() 函数,如果不初始化会报错for (inti=0; i<=target; i++) {
List<List<Integer>>ans_i=newArrayList<List<Integer>>();
ans.add(i, ans_i);
    }
for (inti=0; i<nums.length; i++) {
for (intsum=nums[i]; sum<=target; sum++) {
List<List<Integer>>ans_sum=ans.get(sum);
List<List<Integer>>ans_sub=ans.get(sum-nums[i]);
//刚开始 ans_sub 的大小是 0,所以单独考虑一下这种情况if (sum==nums[i]) {
ArrayList<Integer>temp=newArrayList<Integer>();
temp.add(nums[i]);
ans_sum.add(temp);
            }
//如果 ans.get(sum - nums[i])大小不等于 0,就可以按之前的想法更新了。//每个 ans_sub[j] 都加上 nums[i]if (ans_sub.size() >0) {
for (intj=0; j<ans_sub.size(); j++) {
ArrayList<Integer>temp=newArrayList<Integer>(ans_sub.get(j));
temp.add(nums[i]);
ans_sum.add(temp);
                }
            }
        }
    }
returnans.get(target);
}

对回溯法又有了更深的了解,一般的架构就是一个大的 for 循环,然后先 add,接着利用递归进行向前遍历,然后再 remove ,继续循环。而解法二的动态规划就是一定要找到递进的规则,开始的时候就想偏了,导致迟迟想不出来。

相关文章
|
6月前
leetcode-1518:换酒问题
leetcode-1518:换酒问题
33 0
|
6月前
|
算法
leetcode:389. 找不同
leetcode:389. 找不同
28 0
|
6月前
leetcode-827:最大人工岛
leetcode-827:最大人工岛
62 0
|
6月前
leetcode-1219:黄金矿工
leetcode-1219:黄金矿工
79 0
LeetCode 354. Russian Doll Envelopes
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。 请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
81 0
LeetCode 354. Russian Doll Envelopes
|
测试技术
一和零(LeetCode-474)
一和零(LeetCode-474)
136 0
一和零(LeetCode-474)
|
人工智能 算法
leetcode第41题
对于这种要求空间复杂度的,我们可以先考虑如果有一个等大的空间,我们可以怎么做。然后再考虑如果直接用原数组怎么做,主要是要保证数组的信息不要丢失。目前遇到的,主要有两种方法就是交换和取相反数。
leetcode第41题
leetcode第53题
解法一和解法二的动态规划,只是在定义的时候一个表示以 i 开头的子数组,一个表示以 i 结尾的子数组,却造成了时间复杂度的差异。问题就是解法一中求出了太多的没必要的和,不如解法二直接,只保存最大的和。
leetcode第53题
|
存储
leetcode第56题
常规的思想,将大问题化解成小问题去解决。 假设给了一个大小为 n 的列表,然后我们假设 n - 1 个元素的列表已经完成了全部合并,我们现在要解决的就是剩下的 1 个,怎么加到已经合并完的 n -1 个元素中。 这样的话分下边几种情况, 我们把每个范围叫做一个节点,节点包括左端点和右端点。 1. 如下图,新加入的节点左端点和右端点,分别在两个节点之间。这样,我们只要删除
101 0
leetcode第56题
|
算法
leetcode第32题
这几种算法,暴力破解和动态规划我觉得想的话,还是能分析出来的话,最后两种算法感觉是去挖掘题的本质得到的算法,普适性不是很强。但最后一种算法,从左到右,从右到左,是真的强。
leetcode第32题