说明:问题描述来源leetcode
一、问题描述:
40. 组合总和 II
难度中等1184
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
二、题解:
注意:只有最后一个是正确的,其他为做题过程
分析:先是来一个经典的回溯模板:
public class Solution {
int[] candidates;
int target;
List<List<Integer>> result = new LinkedList<>();
List<Integer> path = new LinkedList<>();
Map<Integer, List<Integer>> map = new HashMap<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
this.candidates = candidates;
this.target = target;
backTracking(0,0);
return result;
}
private void backTracking(int startIndex, int sum) {
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length; i++) {
int sumPlus = sum + candidates[i];
if (sumPlus > target) return;
path.add(candidates[i]);
backTracking(i + 1,sumPlus);
path.remove(path.size() - 1);
}
}
}
但是发现是有些重复的过滤不了:
于是需要过滤,怎么过滤,对如果size相等,那么就和上一个添加进去的相等的size的链表进行比较:
class Solution {
int[] candidates;
int target;
List<List<Integer>> result = new LinkedList<>();
List<Integer> path = new LinkedList<>();
Map<Integer, List<Integer>> map = new HashMap<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
this.candidates = candidates;
this.target = target;
backTracking(0,0);
return result;
}
private void backTracking(int startIndex, int sum) {
if (sum == target) FilterWithPath();
for (int i = startIndex; i < candidates.length; i++) {
int sumPlus = sum + candidates[i];
if (sumPlus > target) return;
path.add(candidates[i]);
backTracking(i + 1,sumPlus);
path.remove(path.size() - 1);
}
}
private void FilterWithPath() {
List<Integer> list = new ArrayList<>(path);
if (!map.containsKey(path.size())){
map.put(path.size(),list);
}else {
List<Integer> beforeList = map.get(path.size());
if (isBeforeListEqualPath(beforeList)) return;
// map.replace(path.size(),list);
map.put(path.size(),list);
}
result.add(list);
}
private boolean isBeforeListEqualPath(List<Integer> beforeList) {
for (int i = 0; i < beforeList.size(); i++) {
if (path.get(i) != beforeList.get(i)) return false;
}
return true;
}
}
但是发现没办法过滤全部
比如:
* 输入:[5,3,2,4,2,5,2,4,3]
* 8
* 输出: [[2,2,4],[2,3,3],[2,2,4],[2,3,3],[3,5],[4,4]]
* 预期结果:[[2,2,4],[2,3,3],[3,5],[4,4]]
这个案例就没办法过滤了,说明上面的过滤不全,那么就只能和全部size相等的来过滤了
那就全部过滤,但是可能会很长时间,因为全部size相等的都要比较一遍
class Solution {
int[] candidates;
int target;
List<List<Integer>> result = new LinkedList<>();
List<Integer> path = new LinkedList<>();
Map<Integer, List<List<Integer>>> map = new HashMap<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
this.candidates = candidates;
this.target = target;
backTracking(0,0);
return result;
}
private void backTracking(int startIndex, int sum) {
if (sum == target) FilterWithPath();
for (int i = startIndex; i < candidates.length; i++) {
int sumPlus = sum + candidates[i];
if (sumPlus > target) return;
path.add(candidates[i]);
backTracking(i + 1,sumPlus);
path.remove(path.size() - 1);
}
}
private void FilterWithPath() {
List<Integer> list = new ArrayList<>(path);
if (!map.containsKey(path.size())){
List<List<Integer>> listInMap = new ArrayList<>();
listInMap.add(list);
map.put(path.size(),listInMap);
}else {
List<List<Integer>> beforeLists = map.get(path.size());
if (isPathInBeforeList(beforeLists)) return;
// map.replace(path.size(),list);
beforeLists.add(list);
}
result.add(list);
}
private boolean isPathInBeforeList(List<List<Integer>> beforeLists) {
//如果遍历时一直都是相等,那么可以直接返回true
//如果有不相等的,那么就break,都不相等,那么就返回false
int flag = 0;
for (int i = beforeLists.size() - 1; i >=0; i--) {
List<Integer> list = beforeLists.get(i);
for (int j = 0; j < list.size(); j++) {
int num = list.get(j);
if (num != path.get(j)) {
flag = 0;
break;
}
flag = 1;
}
if (flag == 1) return true;
}
return false;
}
}
看上去也是超出时间范围了:
提交记录
172 / 176 个通过测试用例
状态:超出时间限制
最后执行的输入:
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
30
果然,倒在黎明前夕。
那怎么办?转换思路?怎么转换?想到一个方法,就是对数组的每一个元素进行统计元素及其元素对应的个数。并且得到不重复元素的数组;
然后呢?遍历不重复数组,数组的个数为N可以说是无限,基于该数组回溯,寻找等于出sum等于目标值的链表,并且在中间加过滤,怎么过滤,就是和元素实际个数相关,也就是基于题39进行过滤。不过太多地方要过滤了,还是看看题解吧,不然就要洗洗睡了。
看了网上的大佬的题解,重新组织自己的思路写出来:
题解:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int target;
int[] candidates;
public List<List<Integer>> combinationSum2( int[] candidates, int target ) {
//为了将重复的数字都放到一起,所以先进行排序
this.target = target;
Arrays.sort( candidates );
this.candidates = candidates;
backTracking( 0 ,0);
return res;
}
private void backTracking( int start ,int sum) {
if ( sum == target ) {
res.add( new ArrayList<>( path ) );
return;
}
for ( int i = start; i < candidates.length ; i++ ) {
if (sum + candidates[i] > target) return;
// if ( i > start && candidates[i] == candidates[i - 1] ) continue;
path.add( candidates[i] );
backTracking( i + 1 ,sum + candidates[i]);
path.removeLast();
}
}
}
那一行注释掉后,应该是一个典型的模板了,
然后取消注释那一行就可以了呢?我们先分析如果注释了会有哪些重复的:
[5,3,2,4,2,5,2,4,3]//2 2 2 3 3 4 4 5 5
8
输出:[[2,2,4],[2,2,4],[2,2,4],[2,2,4],[2,3,3],[2,2,4],[2,2,4],[2,3,3],[2,3,3],[3,5],[3,5],[3,5],[3,5],[4,4]]
预期:[[2,2,4],[2,3,3],[3,5],[4,4]]
思考一下,对于一开始的元素,连续相同的元素是不是从开头取就可以了,后面的相同的元素都不要了,因为首元素开始的范围更大,因此对于连续的重复元素只需要取第一个即可。
如果实在难以理解,可以一步一步得将遍历的路线写出来,
2 2 2 3 3 4 4 5 5
第一层:
<?> <?> <?> <?> <?> <?> <?> <?> <?>
<2> 2 2 3 3 4 4 5 5
<2>
<2>
<3>end
<2>
<3>
<3>end
<2>
<3>
<4>end
<2>
<4>ok
可以再继续下去就知道为什么每一层遍历取连续元素时只取第一个了。
end