回溯章节理论基础:
39. 组合总和
题目链接:https://leetcode.cn/problems/combination-sum/description/
思路:
本题和之前题目的区别就是,本题没有数量要求,可以重复选取,但是有综合的限制,所以间接的也是有个数的限制。
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates,target,0,0);
return result;
}
public void backtracking(int[] candidates,int target, int sum, int startIndex){
if(sum > target)
return ;
if(sum == target){
result.add(new ArrayList<>(paths));
return ;
}
for(int i = startIndex; i< candidates.length; i++){
sum += candidates[i];
paths.add(candidates[i]);
backtracking(candidates, target, sum, i);
paths.removeLast();
sum -= candidates[i];
}
}
}
40. 组合总和II
题目链接:https://leetcode.cn/problems/combination-sum-ii/description/
思路:
这道题目和39.组合总和 (opens new window)如下区别:
本题candidates 中的每个数字在每个组合中只能使用一次。
本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates
同时,本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合。
所以要在搜索的过程中就去掉重复组合。
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)
强调一下,树层去重的话,需要对数组排序!
与上一题套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。
和上一题相同,终止条件为 sum > target 和 sum == target。
这里最大的不同之处,就在于去重了。
前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过
因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上。这个地方的理解很关键!!
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> paths = new ArrayList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
used = new boolean[candidates.length];
Arrays.fill(used,false);
backtracking(candidates,target,0,0);
return result;
}
public void backtracking(int[] candidates, int target, int sum ,int startIndex){
if(sum > target)
return ;
if(sum == target){
result.add(new ArrayList<>(paths));
return ;
}
for(int i=startIndex;i < candidates.length;i++){
// 同层的上一个结点被访问过,就直接跳过
// 这里的used[i - 1] == false,说明是在同一层使用过;
// 如果是true,说明是在同一分支中,这个时候是可以取的
if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false){
continue;
}
used[i] = true;
sum += candidates[i];
paths.add(candidates[i]);
backtracking(candidates,target,sum,i+1);
paths.removeLast();
sum -= candidates[i];
used[i] = false;
}
}
}
131.分割回文串
题目链接:https://leetcode.cn/problems/palindrome-partitioning/
思路:
本题这涉及到两个关键问题:
(1)切割问题,有不同的切割方式
(2)判断回文
我们来分析一下切割,其实切割问题类似组合问题。
例如对于字符串abcdef:
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
所以切割问题,也可以抽象为一棵树形结构,如图:(红线代表切割的位置)
此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。
本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
当切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
在for (int i = startIndex; i < s.length(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入paths中,paths用来记录切割过的回文子串。
注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
判断回文子串是个模板,用双指针即可,这里一开始没想到,以后要形成条件反射!
class Solution {
List<List<String>> result = new ArrayList<>();
List<String> paths = new ArrayList<>();
public List<List<String>> partition(String s) {
backTracking(s,0);
return result;
}
public void backTracking(String s, int startIndex){
// 如果起始位置等于s的大小,说明找到了一组分割方案
if(startIndex == s.length()){
result.add(new ArrayList<>(paths));
return ;
}
// 从startIndex开始切割,i表示切割的位置
for(int i= startIndex; i< s.length();i++){
// 如果是回文的话,则记录
if(isPalindrome(s,startIndex,i)){
String str = s.substring(startIndex,i+1);
paths.add(str);
}else
continue;
// 起始位置后移,保证不重复切割
backTracking(s, i+1);
// 回溯过程,弹出本次已经添加的子串
paths.removeLast();
}
}
// 用双指针法判断是否是回文串
public boolean isPalindrome(String s,int startIndex,int endIndex){
for(int i= startIndex,j=endIndex;i<j;i++,j--){
if(s.charAt(i) != s.charAt(j))
return false;
}
return true;
}
}