39.组合总和
题目描述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例1:
输入:candidates=[2,3,6,7],target=7candidates = [2,3,6,7], target = 7candidates=[2,3,6,7],target=7
输出:[[2,2,3],[7]][[2,2,3],[7]][[2,2,3],[7]]
示例2:
输入:candidates=[2,3,5],target=8candidates = [2,3,5], target = 8candidates=[2,3,5],target=8
输出:[[2,2,2,2],[2,3,3],[3,5]][[2,2,2,2],[2,3,3],[3,5]][[2,2,2,2],[2,3,3],[3,5]]
示例3:
输入:candidates=[2],target=1candidates = [2], target = 1candidates=[2],target=1
输出:[][][]
思路
本题仍然是回溯组合的应用,仔细观察可以发现其中的不同:
1、candidates的元素不重复,但其中的数字可以无限使用,也就是说,在同一个组合中可以多次使用同一个元素,但返回的列表中不能存在相同的组合。
2、组合仍然不关注顺序,即1,2和2,1仍然为同一个组合。
那么如何表达元素在一个结果中可以无限使用呢,在每一次递归的时候不+1即可,也就是说,每次的递归选择中,无需从下一位数字选取,而是可以仍然选择当前的数字。
下面有需要考虑一个问题:递归的过程如何跳出
按照题目的要求,应该是和大于或等于target的时候即可跳出,其中如果相等需要记录该组合。
为了不将所有可能全部遍历,可以进行剪枝,那么如何剪枝呢?
可以看出,能从大于的情况进行剪枝的操作。当目前的和大于target之后这一支之后是不可能出现符合要求的组合的,需要注意的是,这样剪枝的前提是数组有序。故而需要先排序。
题解
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
helper(res,new ArrayList<>(),candidates,target,0,0);
return res;
}
public void helper(List<List<Integer>> res,List<Integer> path,int[] candidates,
int target,int sum,int index){
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
for(int i = index;i<candidates.length;i++){
if(sum + candidates[i] > target) break;
path.add(candidates[i]);
helper(res,path,candidates,target,sum+candidates[i],i);
path.remove(path.size()-1);
}
}
}
总结
本题主要有两个难点:
1、无限制重复选取在递归中应该如何表达
2、剪枝操作的使用
40.组合总和Ⅱ
题目描述
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例1:
输入:candidates=[10,1,2,7,6,1,5],target=8,candidates = [10,1,2,7,6,1,5], target = 8,candidates=[10,1,2,7,6,1,5],target=8,
输出:[[1,1,6],[1,2,5],[1,7],[2,6]][
[1,1,6],
[1,2,5],
[1,7],
[2,6]
][[1,1,6],[1,2,5],[1,7],[2,6]]
示例2:
输入:candidates=[2,5,2,1,2],target=5,candidates = [2,5,2,1,2], target = 5,candidates=[2,5,2,1,2],target=5,
输出:[[1,2,2],[5]][
[1,2,2],
[5]
][[1,2,2],[5]]
思路
本题的特色在于,有重复的元素,每个数字只能在每个组合中使用一次,同时仍然要求不能有重复的组合。
那么本题的重点自然也就是剪枝的操作,问题在于,剪哪里的枝
仔细思考可以发现,这些限制用常规的理解来表达就是,每一个数字用过了之后就不能再用了,但是两个相同的数字算作两个不同的个体。
那么能够快速想到的方法就是,对每个元素是否已经用在了此时的组合里进行记录,在使用一个元素时首先查询,若用过了就不能再用了。
而还有一种方法就是,跳过相同的元素,也就是,在递归循环的过程中,其实每个元素是都会被遍历一遍的,那么就可以通过在遍历的时候,直接使用startindex而避免使用之前已经使用过的元素。
题解1
class Solution {
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
boolean[] used;
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
Arrays.fill(used,false);
Arrays.sort(candidates);
helper(candidates,target,0);
return res;
}
public void helper(int[] candidates,int target,int startIndex){
if(sum == target){
res.add(new ArrayList(path));
}
for(int i = startIndex;i<candidates.length;i++){
if(sum + candidates[i] > target){
break;
}
if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1]){
continue;
}
used[i] = true;
sum += candidates[i];
path.add(candidates[i]);
helper(candidates , target,i+1);
used[i] = false;
sum -= candidates[i];
path.removeLast();
}
}
}
题解2
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
helper(candidates , target ,0);
return res;
}
public void helper(int[] candidates,int target,int start){
if(sum == target){
res.add(new ArrayList<>(path));
return ;
}
for(int i = start; i < candidates.length && sum + candidates[i] <= target;i++){
if(i > start && candidates[i] == candidates[i-1]){
continue;
}
sum += candidates[i];
path.add(candidates[i]);
helper(candidates , target , i+1);
int tmp = path.getLast();
sum -= tmp;
path.removeLast();
}
}
}
总结
本题值得再多看几遍,这个剪枝操作的思考量真的很精彩
131.分割回文串
题目描述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
思路
本题涉及到了回溯的另一个应用方向——分割。在本题应该有两个部分的设计:
1、如何分割,即,分割的过程中,那一道分割线如何表达
2、是否回文
首先是如何分割,分割的回溯过程本质上和组合差不多,只不过每一层中切割的都是不同数量的元素从而形成树形结构。而分割线也可以使用我们熟悉的startindex来表示。
是否回文在本题中是在判断一个结果是否有被保存的资格,那么可以独立作为一个判断的方法,调用在递归中。
题解
class Solution {
List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
helper(s,0);
return res;
}
public void helper(String s , int startIndex){
if(startIndex >= s.length()){
res.add(new ArrayList(path));
return;
}
for(int i = startIndex;i< s.length();i++){
if(isPalindrome(s,startIndex,i)){
String string = s.substring(startIndex,i+1);
path.addLast(string);
}
else{
continue;
}
helper(s,i+1);
path.removeLast();
}
}
public boolean isPalindrome(String s, int startIndex, int end){
for(int i = startIndex,j = end; i < j;i++,j--){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
}
总结
突然进入分割的题目还不太会,还是需要多看看多练练。
文章讲述了使用回溯算法解决组合总和的问题,包括无重复元素的数组中找到所有加和为目标数的组合,以及考虑元素可重复使用的优化。重点在于回溯过程中的剪枝操作,确保结果不包含重复组合。此外,还介绍了处理有重复元素的组合总和问题,通过跳过相同元素避免重复组合。最后提到了分割回文串的问题,利用回溯法判断子串是否为回文并进行分割。
70

被折叠的 条评论
为什么被折叠?



