39. 组合总和
这题和前面做过的题类似,重点注意同一个 数字可以 无限制重复被选取和无重复元素。
- 首先确定回溯的出入参数,同样用一个result数组去存储最后的返回结果,与此同时path记录下回溯过程中走过的每一条路径的结果集。前面做到的题目用一个startIndex去记录遍历循环的次数。二刷这道题的时候我在想这个startIndex的真正意义是什么,因为它是用来控制循环起始位置的,所以组合回溯的时候,进入下一次回溯我们就会在原来循环的次数上+1。此外还需要sum变量来统计单一结果path里的总和,这个sum可用可不用,如果不用的话就一直用target减去数组内的值然后判断target==0就好了。最后,对于组合问题,什么时候需要startIndex呢? 如果是一个集合来求组合的话,就需要startIndex,例如:[77.组合 ],[216.组合总和III]。 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[17.电话号码的字母组合]
- 递归的终止条件:当sum>target的时候可以直接返回了,如果sum==target成立那么说明找到了一条path是符合要求的答案,可以直接添加到result结果集中。
- 单层搜索逻辑:这道题和[77.组合]、[216.组合总和III]的一个区别是:本题元素为可重复选取的。这意味着我们在回溯遍历时不用i+1了,表示可以重复读取当前的数。
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
combinationSumHelper(candidates, target, 0, 0);
return res;
}
public void combinationSumHelper(int[] candidates, int target, int startIndex, int sum){
if(sum > target) return;
if(sum == target){
res.add(new ArrayList(path));
return;
}
for(int i = startIndex; i < candidates.length; i ++){
//if(sum + candidates[i] > target) break;
sum += candidates[i];
path.add(candidates[i]);
combinationSumHelper(candidates, target, i, sum);
sum -= candidates[i];
path.removeLast();
}
}
}
40. 组合总和 II
这道题目和39. 组合总和如下区别:
- 本题candidates 中的每个数字在每个组合中只能使用一次。
- 本题数组candidates的元素是有重复的,而39.组合总和 是无重复元素的数组candidates
如果选择先用回溯把所有组合情况求出来,然后再用hashset去重的话很容易超时,所以只能在回溯的过程中就实现去重步骤。组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。这里同一树枝上使用过其实是指同一个数字可否被反复选择,所以这道题的去重指的是同一树层上是否被使用过。
- 确定函数出入参:和39题的出入参相同,这里还需要加一个bool型数组isused,用来记录同一树枝上的元素是否使用过。
- 递归终止条件:终止条件为
sum > target和sum == target。 - 单层循环逻辑:因为去重的是同一树层里使用过的,那我们判断同一树层上元素(相同的元素)是否使用过的标准就是这个isused==false(因为回溯一轮结束后上一个元素会被回溯回false)。所以这里的判断条件就是
candidates[i] == candidates[i - 1]并且isused[i - 1] == false,条件符合时就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] isused;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
isused = new boolean[candidates.length];
Arrays.sort(candidates);
for(int i = 0; i < candidates.length; i ++) isused[i] = false;
backTracing(candidates, target, 0, 0);
return res;
}
public void backTracing(int[] candidates, int target, int startIndex, int sum){
if(sum > target) return;
if(sum == target){
res.add(new ArrayList(path));
return;
}
for(int i = startIndex; i < candidates.length; i ++){
//if(sum + candidates[i] > target) break;
// 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
if (i > 0 && candidates[i] == candidates[i - 1] && !isused[i - 1]) {
continue;
}
sum += candidates[i];
isused[i] = true;
path.add(candidates[i]);
backTracing(candidates, target, i + 1, sum);
sum -= candidates[i];
isused[i] = false;
path.removeLast();
}
}
}
131. 分割回文串
这道题涉及到 1.切割字符串 2.判断是否回文串
切割问题其实类似组合问题,切割出第一个字符以后从剩下的字符中往后切割,其实就相当于从剩下的字符中组合出符合题意的字符串,所以同样可以抽象成树形结构。
- 递归传入的出入参:全局变量数组path存放切割后回文的子串,二维数组result存放结果集。还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
- 递归终止条件:切割线切到了字符串的尾部说明切割完成了,找到了对应的回文串。
- 单层逻辑:我们在切割过程中定义了起始位置startIndex,那么[startIndex, i]就是切割的区间,与此同时我们需要判断这个子串是不是回文,如果是回文,就记录到path集合中去。注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
class Solution {
List<List<String>> res = new ArrayList<>();
Deque<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backtraing(s, 0);
return res;
}
public void backtraing(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 str = s.substring(startIndex, i + 1);
path.add(str);
} else{
continue;
}
backtraing(s, i + 1);
path.removeLast();
}
}
private boolean isPalindrome(String s, int start, int end){
for(int i = start, j = end; i < j; i ++, j --){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
}