39. 组合总和
力扣链接:力扣
本题和77.组合 (opens new window),216.组合总和III (opens new window)的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。
且题目中不用担心0怎么办,因为1 <= candidates[i] <= 200。
因此树形图可以抽象为:
这里递归的时候就没有层数限制,超过target就返回。
回溯三部曲:
函数的输出和输入:输出- viod;输入- 【全局变量 List<Integer> list - 符合条件的集合;List<List<Integer>> result - 全部的结果】,int[] candidates, int index - 遍历的起始位置,int sum - 总和, int target
终止条件:当sum > target,那么就直接返回;如果sum == target,那么就将符合条件的集合放入结果集
单层循环逻辑: for循环(int i = index;i < candidates.length; i++), 将index位置的元素加入list,sum加上index位置的元素; backtraking(candidates, index, sum, target) -- 因为是重复添加,因此不需要+1; 回溯 - sum -= list.get(list.length-1), list.remove(list.length-1)。
具体代码实现:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
list = new ArrayList<>();
result = new ArrayList<>();
backtraking(candidates, target, 0, 0);
return result;
}
List<Integer> list;
List<List<Integer>> result;
public void backtraking(int[] candidates, int target, int sum, int index){
if(sum > target){
return;
}
if(sum == target){
result.add(new ArrayList<Integer>(list));
return;
}
for(int i = index; i<candidates.length; i++){
sum += candidates[i];
list.add(candidates[i]);
backtraking(candidates, target, sum, i);
//回溯
sum -= list.get(list.size() -1);
list.remove(list.size() -1);
}
}
}
剪枝优化方案:
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
那么就需要先对数组进行排序。
具体代码实现:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
list = new ArrayList<>();
result = new ArrayList<>();
Arrays.sort(candidates);
backtraking(candidates, target, 0, 0);
return result;
}
List<Integer> list;
List<List<Integer>> result;
public void backtraking(int[] candidates, int target, int sum, int index){
if(sum > target){
return;
}
if(sum == target){
result.add(new ArrayList<Integer>(list));
return;
}
//剪枝优化
for(int i = index; i<candidates.length && sum + candidates[i] <= target; i++){
sum += candidates[i];
list.add(candidates[i]);
backtraking(candidates, target, sum, i);
//回溯
sum -= list.get(list.size() -1);
list.remove(list.size() -1);
}
}
}
参考资料:代码随想录
40.组合总和II
题目链接:力扣
这道题不一样的就是,集合中有重复的元素,但是组合中是不能重复的,因此得进行去重。【注意组合中的元素是可以重复的如果存在重复的元素的话】
可以不去重,直接用hashset来储存组合,最后转换成list --- 但是这样的做法非常繁琐,且容易超时。
如何在搜索过程中去掉重复的元素呢?
所谓去重,其实就是使用过的元素不能重复选取。
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了 - 为了让相邻的元素都放在一起,这样会包含对这个元素所有组合的情况)
回溯三部曲:
1.输出和输入参数:输出 - void; 输入 - 【两个全局变量 List<Integer> list, List<List<Integer>> result】,int[] candidates, int target, int sum, int index;注意因为要去重,因此需要一个boolean数组 boolean[] used 「针对树层去重的操作」
2.终止条件:如果sum > target, 返回;如果sum == target,将list加入结果集
3.单层循环逻辑:
去重操作:
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
for(int i= index; i<candidates.length; i++); 将index位置的元素放入list,将index位置的元素加入sum,used[i-1] == true;backtraking(candidates, target, sum, index+1); 回溯 - sum,list, used。
具体代码:
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
result = new ArrayList<>();
list = new ArrayList<>();
used = new boolean[candidates.length];
//排序
Arrays.sort(candidates);
backtraking(candidates, target, 0, 0, used);
return result;
}
List<List<Integer>> result;
List<Integer> list;
boolean[] used;
public void backtraking(int[] candidates, int target, int sum, int index, boolean[] used){
if(sum > target){
return;
}
if(sum == target){
result.add(new ArrayList<>(list));
return;
}
//剪枝操作
for(int i = index; i < candidates.length && sum + candidates[i] <= target; i++){
//去重
if(i>0 && candidates[i-1] == candidates[i] && used[i-1] == false){
continue;
}
sum += candidates[i];
list.add(candidates[i]);
used[i] = true;
backtraking(candidates, target, sum, i+1, used);
//回溯
sum -= list.get(list.size()-1);
list.remove(list.size()-1);
used[i] = false;
}
}
}
参考资料:代码随想录
131.分割回文串
题目链接:力扣
本题这涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
切割问题类似组合问题。
例如对于字符串abcdef:
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。
回归三部曲:
1.确定输出和输入参数:输出-void; 输入参数 - 【两个全局变量:List<List<String>> result; List<String> path - 单一结果】,backtraking(String s, int index-确定从哪个位置继续切割)
2.终止条件: 切割到字符串(index == s.length)最后,将path加入到result中,就返回;
3.单层搜索逻辑:
for(int i = index; i<s.length(); i++);找到切割的子串 - [index,i] - 判断这个子串是否为回文子串「定义一个新的函数isPalendrom(String s, int index, int i)」; 如果是回文的话,就用path进行收集,如果不是回文子串,则直接continue;
递归 - backtraking(s, i+1)
回溯 - path删除
具体代码实现:
class Solution {
public List<List<String>> partition(String s) {
result = new ArrayList<>();
path = new ArrayList<>();
backtraking(s, 0);
return result;
}
List<List<String>> result;
List<String> path;
public void backtraking(String s, int index){
//终止条件
if(index == s.length()){
result.add(new ArrayList<>(path));
return;
}
for(int i=index; i<s.length(); i++){
if(isPalindrome(s, index, i)){
path.add(s.substring(index,i+1));
}else{
continue;//如果不是回文子串则继续向后切割
}
backtraking(s, i+1);//只有找到回文子串才会进行递归搜索
//回溯
path.remove(path.size()-1);
}
}
public boolean isPalindrome(String s, int left, int right){
for(int i = left, j = right; i<j; i++, j--){//两个变量进行for循环,可以参考下
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
}
参考资料:代码随想录