【题目描述】
给定一个候选人编号的集合
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
【题目链接】. - 力扣(LeetCode)
【解题代码】
package dp;
import java.util.*;
public class CombinationSum2 {
private static List<List<Integer>> numLists = new ArrayList<>();
private static List<Integer> numList = new ArrayList<>();
public static void main(String[] args) {
//int[] candidates = {10, 1, 2, 7, 6, 1, 5};
//int[] candidates = {10, 1, 2, 7, 6, 1, 5};
int[] candidates = {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, 2};
System.out.println("开始计算。。。");
long start = System.currentTimeMillis();
List<List<Integer>> numLists = new CombinationSum2().combinationSum(candidates, 8);
System.out.println("运行时长:" + (System.currentTimeMillis() - start) + "ms");
for (List<Integer> numList : numLists) {
System.out.println(Arrays.toString(numList.toArray()));
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
doCombinationSum(candidates, 0, target);
HashSet set = new HashSet<>(numLists);
numLists.clear();
numLists.addAll(set);
return numLists;
}
private void doCombinationSum(int[] candidates, int n, int target) {
if (target == 0) {
numLists.add(new ArrayList<>(numList));
} else if (n < candidates.length) {
doCombinationSum(candidates, n + 1, target);
if (target >= candidates[n]) {
int i = n + 1;
int delta = candidates[n];
numList.add(candidates[n]);
while (i < candidates.length && candidates[i] == candidates[n] && target >= delta) {
delta += candidates[i];
numList.add(candidates[n]);
i++;
}
doCombinationSum(candidates, i, target - delta);
for (int j = i; j > n; j--) {
numList.remove(numList.size() - 1);
}
}
}
}
private void doCombinationSum1(int[] candidates, int n, int target) {
if (target == 0) {
numLists.add(new ArrayList<>(numList));
} else if (n < candidates.length) {
if (target >= candidates[n]){
doCombinationSum(candidates, n + 1, target);
numList.add(candidates[n]);
doCombinationSum(candidates, n + 1, target - candidates[n]);
numList.remove(numList.size() - 1);
}
}
}
}
【解题思路】
一开始拿到题目,以为和之前解析的LeetCode39题: 组合总和(原创)-CSDN博客差不多,直接把之前代码拷贝,做了一些排序,去重相关的修改,提交运行成功,代码如下:
class Solution {
private List<List<Integer>> numLists = new ArrayList<>();
private List<Integer> numList = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
doCombinationSum(candidates, 0, target);
HashSet set = new HashSet<>(numLists);
numLists.clear();
numLists.addAll(set);
return numLists;
}
private void doCombinationSum(int[] candidates, int n, int target) {
if (target == 0) {
numLists.add(new ArrayList<>(numList));
} else if (n < candidates.length) {
doCombinationSum(candidates, n + 1, target);
if (target >= candidates[n]) {
numList.add(candidates[n]);
doCombinationSum(candidates, n + 1, target - candidates[n]);
numList.remove(numList.size() - 1);
}
}
}
}
以为和之前题目一样,没什么问题了,但已提交出乎意料,系统跑到第172个测试用例:30个重复的1时,提示超出时间限制:
自己拿这个测试用例在本地跑了下,运行时长7111ms,确实性能有问题。那如何优化,当时思考和实现了多次没有头绪,于是放弃了。一直到今天想尽量清理提交未通过时的题目时,对此题思考了一会,突然有了思路:对于重复的数据,全选的情况其实是唯一,那何不一次性将所有的重复数据加入结果集中,而避免一个一个的递归。这样性能应该能大大提升。想到一点,感觉有希望,如实按照这个思路快速修改代码,果然所有测试用例都过关了
虽然数据还不是特别好,但终于算过关了。
【解题步骤】
- 在解题类里定义两个静态链表对象:一个numList存储当前组合数据列表,一个numLists存储所有组合列表
private static List<List<Integer>> numLists = new ArrayList<>(); private static List<Integer> numList = new ArrayList<>();
- 定义一个回溯递归函数doCombinationSum,参数包括整数数组 candidates,当前索引值n,目标值targe
private void doCombinationSum(int[] candidates, int n, int target) {
- 因为存在重复数据,先将所有数据进行排序
Arrays.sort(candidates);
- 如果目标值target为0,说明当前候选数字序列numList满足要求,添加到结果集numLists
if (target == 0) { numLists.add(new ArrayList<>(numList)); }
- 如果数组遍历还没遍历完,首先选择不选择当前数字,直接递归处理下一个索引数字即可
} else if (n < candidates.length) { doCombinationSum(candidates, n + 1, target, numList, numLists);
- 接下来,如果当前数字小于等于目标值target,那么尝试一次性选择所有相同数值的数字,并将此数据序列添加到候选数字序列,将目标值target去数值后,并从当前索引重复递归,递归完毕,回溯将此数据序列从候选数字序列中删
if (target >= candidates[n]) { int i = n + 1; int delta = candidates[n]; numList.add(candidates[n]); while (i < candidates.length && candidates[i] == candidates[n] && target >= delta) { delta += candidates[i]; numList.add(candidates[n]); i++; } doCombinationSum(candidates, i, target - delta); for (int j = i; j > n; j--) { numList.remove(numList.size() - 1); } }
【思考总结】
- 这一题是LeetCode39题: 组合总和(原创)-CSDN博客的姊妹题,其关键解题思路:对于重复的数据,全选的情况其实是唯一,那何不一次性将所有的重复数据加入结果集中,而避免一个一个的递归。这样性能应该能大大提升。
- 了解掌握回溯算法定义:回溯算法定义 回溯算法,是一种选优搜索法,又称为试探法,按选优条件向前搜索以达到目标。 回溯算法简要说:但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。 而满足回溯条件的某个状态的点称为“回溯点”。
- 回溯与递归的区别:递归的基本性质就是函数调用,在处理问题的时候,递归往往是把一个大规模的问题不断地变小然后进行推导的过程。 回溯则是利用递归的性质,从问题的起始点出发,不断地进行尝试,回头一步甚至多步再做选择,直到最终抵达终点的过程;
- LeetCode解题之前,一定不要看题解,看了就“破功”了!