https://leetcode.com/problems/combination-sum-ii/description/
Given a collection of candidate numbers (C) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
Each number in C may only be used once in the combination.
Note:
- All numbers (including target) will be positive integers.
- Elements in a combination (a1, a2, … , ak) must be in non-descending order. (ie, a1 ≤ a2 ≤ … ≤ ak).
- The solution set must not contain duplicate combinations.
For example, given candidate set 10,1,2,7,6,1,5
and target 8
,
A solution set is: [1, 7]
[1, 2, 5]
[2, 6]
[1, 1, 6]
解题思路:
这道题看似和上一题很像,只是答案里每个元素只能用一次了,(上一题是每个元素可以用多次)。其实还有一个比较大的变化,就是上一题的数组是set,而这一题是collection。也就是说,这里,可能给定的数组里,元素就有重复。
所以开始我只想到,上一题递归的时候,是从当前元素开始往后搜,现在只需要改成从当前元素的下一个就开始了。现在当前元素的下一个元素可能也与当前元素相同,当然不行了。
于是又写了一个前面做过的题目的removeDuplicate的方法,想把元素组去重,结果当然是错误的,上面的例子里[1, 1, 6]这个答案就没了。
代码还是给一下吧,错误。
public class Solution { public List<List<Integer>> combinationSum2(int[] num, int target) { List<List<Integer>> resultList = new LinkedList<List<Integer>>(); List<Integer> currentList = new LinkedList<Integer>(); //原题没说数组已经排序了,必须先排序 Arrays.sort(num); num = removeDuplicate(num); dfs(resultList, currentList, 0, 0, num, target); return resultList; } public int[] removeDuplicate(int[] num){ if(num.length == 0){ return new int[] {}; } int swapIndex = 1; int pre = num[0]; for(int i = 1; i < num.length; i++){ if(num[i] > pre){ pre = num[i]; num[swapIndex] = num[i]; swapIndex++; } } return Arrays.copyOf(num, swapIndex); } public void dfs(List<List<Integer>> resultList, List<Integer> currentList, int currentSum, int step, int[] num, int target){ if(currentSum == target){ resultList.add(new LinkedList(currentList)); return; } if(currentSum > target){ return; } //避免结果重复的方法:i从当前元素的后一个元素往后开始,所以要把当前元素作为参数传进方法 for(int i = step; i < num.length; i++){ currentList.add(num[i]); currentSum += num[i]; dfs(resultList, currentList, currentSum, i + 1, num, target); currentList.remove(currentList.size() - 1); currentSum -= num[i]; } } }
后来我看了一下List的equals方法,是重写好的,会自动去判断每个元素是否是值相等。所以我可以放心的调用List.contains()的方法了,只有不含有当前答案时,才塞进去。否则[1,1],1,的时候,会塞入两个1。
public class Solution { public List<List<Integer>> combinationSum2(int[] num, int target) { List<List<Integer>> resultList = new LinkedList<List<Integer>>(); List<Integer> currentList = new LinkedList<Integer>(); //原题没说数组已经排序了,必须先排序 Arrays.sort(num); dfs(resultList, currentList, 0, 0, num, target); return resultList; } public void dfs(List<List<Integer>> resultList, List<Integer> currentList, int currentSum, int step, int[] num, int target){ if(currentSum == target){ if(!resultList.contains(currentList)){ resultList.add(new LinkedList(currentList)); } return; } if(currentSum > target){ return; } //避免结果重复的方法:i从当前元素的后一个元素往后开始,所以要把当前元素作为参数传进方法 for(int i = step; i < num.length; i++){ currentList.add(num[i]); currentSum += num[i]; dfs(resultList, currentList, currentSum, i + 1, num, target); currentList.remove(currentList.size() - 1); currentSum -= num[i]; } } }
可是上面解法的时间复杂度偏高,要350ms。能不能像上面那样,在搜索元素的时候就避免重复的结果?
联想到前面很多去重的例子,比如Search in Rotated Sorted Array II,比如3Sum,一般都是比较相邻两个元素,如果相等,都略过当前元素,指针前移。这是一个很重要的去重手段,远比最后比较结果是否重复来的有效!
于是我写了下面的代码,遇到了如下错误。
Input: [1,1], 2
Output: []
Expected: [[1,1]]
public class Solution { public List<List<Integer>> combinationSum2(int[] num, int target) { List<List<Integer>> resultList = new LinkedList<List<Integer>>(); List<Integer> currentList = new LinkedList<Integer>(); //原题没说数组已经排序了,必须先排序 Arrays.sort(num); dfs(resultList, currentList, 0, 0, num, target); return resultList; } public void dfs(List<List<Integer>> resultList, List<Integer> currentList, int currentSum, int step, int[] num, int target){ if(currentSum == target){ resultList.add(new LinkedList(currentList)); return; } if(currentSum > target){ return; } //避免结果重复的方法:i从当前元素的后一个元素往后开始,所以要把当前元素作为参数传进方法 for(int i = step; i < num.length; i++){ if(i > 0 && num[i] == num[i - 1]){ continue; } currentList.add(num[i]); currentSum += num[i]; dfs(resultList, currentList, currentSum, i + 1, num, target); currentList.remove(currentList.size() - 1); currentSum -= num[i]; } } }
为什么?递归到第二个元素,也就是step==1时,因为当前num[1]==num[0],直接被跳过了!这属于什么样的处理?也就是说当前元素和上一元素相等,就跳过。上一元素是刚刚被加入到currentList里的元素。也就是说,这是和currentList里的元素比。如果题目要求,每个组合内的元素都不能重复,才应该这样写。
而这道题的要求是,每个元素只能使用一次!并不是元素的值不能重复,因为num内本来就可能有重复的值。
所以for循环第一句的判断应该是
if(i > step && num[i] == num[i - 1]){ continue; }
这句话的意思是,当前位置的元素如果在本回合内已经被选择过,就不要再加入了。而不是,currentList如果已经有了,就不要加入了。这是完全不同的!
想象一下1,1,1,1,2,从[1]到[1, 1]的过程。step==1时,第一次加入1是可以的,第二次再加入1就不行了。因为DFS,前面的基础是一样的,加入同样的元素必然导致同样的结果。这就是避免重复结果的原理。
public class Solution { public List<List<Integer>> combinationSum2(int[] num, int target) { List<List<Integer>> resultList = new LinkedList<List<Integer>>(); List<Integer> currentList = new LinkedList<Integer>(); //原题没说数组已经排序了,必须先排序 Arrays.sort(num); dfs(resultList, currentList, 0, 0, num, target); return resultList; } public void dfs(List<List<Integer>> resultList, List<Integer> currentList, int currentSum, int step, int[] num, int target){ if(currentSum == target){ resultList.add(new LinkedList(currentList)); return; } if(currentSum > target){ return; } //避免结果重复的方法:i从当前元素的后一个元素往后开始,所以要把当前元素作为参数传进方法 for(int i = step; i < num.length; i++){ if(i > step && num[i] == num[i - 1]){ continue; } currentList.add(num[i]); currentSum += num[i]; dfs(resultList, currentList, currentSum, i + 1, num, target); currentList.remove(currentList.size() - 1); currentSum -= num[i]; } } }
这么一改,时间只需要270ms了。
leetcode 100题完成。