组合总数(回溯)
题目描述:
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
andidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ] 示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]
分析,这题和组合总数Ⅰ有所不同,上一题是任意一个数可以出现任意多次并且形式上没有重复的。
但是这题有重复的但是每个数只能使用一次。如果按照上一题的分析思路就是就是回溯的过程需要进行下一个,并且需要一个哈希对结果去重。
但是我们都知道哈希虽然效率还行但是频繁的判断很影响算法的效率,有什么好的方法去解决?我们在回溯算法上进行合理剪枝。用一个策略去重。
首先对于这个序列,我们先进行排序,排完序之后我们进行一下分析:
怕重复?无非是怕以下的这种情况嘛:
而我们在实际处理的时候,同一个元素如果用多次,只能从第一个元素顺序使用而不能跳跃使用。
在具体的处理方式我,刚开始我用了比较笨的方法:每次找到这个元素找到它相同的区间。然后遍历这个区间排列所有个数(从左向右),同时判断满足条件(数值总和不超出的情况下),递归下一个节点到下一个数值的编号上。当然不使用该元素在最后特殊进行一次dfs即可。
List<Integer> list = new ArrayList<Integer>();// list用来回溯过程中储存元素 List<List<Integer>> val = new ArrayList<List<Integer>>();// 结果 public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); dfs(0, candidates, target); return val; } /* * index 表示当前编号 count 表示当前数值的和 */ private void dfs(int index, int[] candidates, int target) { // System.out.println(list.toString()); if (target == 0) { val.add((new ArrayList<Integer>(list))); return; } else if (index == candidates.length) { return; } else { int r = index; while (r < candidates.length && candidates[index] == candidates[r]) { r++; } int i; for (i = index; i < r; i++) { target -= candidates[i]; list.add(candidates[i]); if (target>=0) { dfs(r, candidates, target); } else {//没有执行dfs但是数值需要恢复 因为i还是变了 target += candidates[index]; list.remove(list.size() - 1); break; } } for (; i > index; i--) { target += candidates[index]; list.remove(list.size() - 1); } dfs(r, candidates, target);// 不使用该元素的 } }
算法 4ms不太行,因为提前计算出右r的位置的时候进行一部分重复量挺大的运算。参考了别人的代码,才发现具体处理上可以这么处理:
遍历时候正常遍历,只不过进行遍历的时候判断如果该元素不是这次首元素的话和前面元素相等那么直接跳过。否则正常进行dfs。这样就可以过滤掉所有重复情况:
最终ac的代码为:
List<Integer> list = new ArrayList<Integer>();// list用来回溯过程中储存元素 List<List<Integer>> val = new ArrayList<List<Integer>>();// 结果 public List<List<Integer>> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); dfs(0, candidates, target); return val; } /* * index 表示当前编号 count 表示当前数值的和 */ private void dfs(int index, int[] candidates, int target) { if (target == 0) { val.add((new ArrayList<Integer>(list))); return; } for(int i=index;i<candidates.length;i++) { if(target-candidates[i]<0)break; if(i!=index&&candidates[i]==candidates[i-1])continue; list.add(candidates[i]); dfs(i+1, candidates, target-candidates[i]); list.remove(list.size()-1); } }
缺失的第一个正数
对于这题,如果不要求其他的,给一个数组空间的话,可以使用数组作为索引,直接干0ms:
public int firstMissingPositive(int[] nums) { int time[]=new int[nums.length+1]; for(int i=0;i<nums.length;i++) { if(nums[i]>0&&nums[i]<nums.length) { time[i]++; } } for(int i=1;i<time.length;i++) { if(time[i]==0) return i; } return nums.length; }
但问题是人家要求的是常数级别空间。还是没想出来忍不住看了下题解恍然大悟:
数组妙用。分析问题,求未出现的最小正数,也就是最理想的情况是1.否则就找个未出现的最小的。这题核心的想法是复用数组,我们只需标记这个位置的数字有没有出现过! 所以可以用负数表示。
按照以下三点分析,首先我们知道这个数组中可能有负数或者大于数组长度数值的数存在,我们第一次遍历要将这些数置为1,并且看看1是否存在。如果不存在直接返回1即可。
第二次遍历,将对应编号位置数改为负数(注意初始正负)。注意取值。
第三次遍历找到第个为正数的那个位置,返回编号加1.如果为正数说明这个数未出现。
具体ac代码为:
public int firstMissingPositive(int[] nums) { boolean jud =false; //第一轮循环,将无用的数字置为1 如果整个数列中没有1 那么将返回1 for(int i=0;i<nums.length;i++) { if(nums[i]<=1||nums[i]>nums.length) { if(nums[i]==1)jud=true; nums[i]=1; } } if(!jud)return 1; //第二轮循环,所有数字现在目前的状态是1 - nums.length区间内 for(int i=0;i<nums.length;i++) { int index=Math.abs(nums[i])-1;//该位置对应的索引 if(nums[index]>0) {nums[index]=-nums[index];} } for(int i=0;i<nums.length;i++) { if(nums[i]>0)return i+1; } return nums.length+1; }
接雨水
题目分析
遇到这种题总有那么种似曾分析的感觉,我来谈谈如何分析这道题。
我的个人思路来说,我觉得这道题的核心就是看看能不能找到中间最高的元素。
找到这个最高点后:
- 左右进行相等操作,从左来看,需要一个
l
和lteam
,让lteam往右试探. - 试探途中如果height[lteam]<height[l],继续前进,后面一定会有更大的。
- 试探途中如果height[lteam]>height[l],计算途中雨点和,然后l赋值为lteam,继续试探。
而同样右侧是进行一个从右往左的操作。
不过再具体实现上,本人初次只考虑从左往右然后遇到没有比它更大的找个次大的计算雨水。然后重新进行。具体代码为:
public int trap(int[] height) { int l=0,r=l+1; int count=0; int max=0,maxindex=0; while (l<height.length&&r<height.length) { if(height[r]>=height[l])//找到更大的靠山 { for(int i=l;i<r;i++) { count+=height[l]-height[i]; } l=r; r++; max=0;maxindex=0; } else {//不大于 if(height[r]>=max) { max=height[r]; maxindex=r; } r++; if(r==height.length) { for(int i=l+1;i<maxindex;i++) { count+=max-height[i]; } l=maxindex;r=l+1; max=0;maxindex=0; } } } return count; }
效果一般般因为遇到逆序形的会O(n2)算法。最好情况O(n)。
而换一种O(n)的写法之后,找到最大后右侧往左一次即可:
public int trap(int[] height) { int l=0,r=l+1; int count=0; while (l<height.length&&r<height.length) { if(height[r]>=height[l])//找到更大的靠山 { for(int i=l;i<r;i++) { count+=height[l]-height[i]; } l=r;r=l+1; } else {//不大于 r++; if(r==height.length)//说明当前的left已经是天底下最大的值了 { int max=0; for(int i=r-1;i>l;i--) { if(height[i]>max) { max=height[i]; } else { count+=max-height[i]; } } l=r; } } } return count;
效果还行:
结语
原创不易,bigsai请你帮两件事帮忙一下:
- star支持一下, 您的肯定是我在平台创作的源源动力。
- 微信搜索「bigsai」,关注我的公众号,不仅免费送你电子书,我还会第一时间在公众号分享知识技术。加我还可拉你进力扣打卡群一起打卡LeetCode。