39. 组合总和
本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制题目链接/文章讲解:代码随想录
重点:
1. startIndex的作用
startIndex是为了控制candidates的大小,如果没有startIndex,那么candidates的长度将一直保持为原始长度,并且后续的递归也会得出前面递归的相同的结果,出现重复的结果。
2. 什么时候需要使用startIndex?
只对于求组合来说。如果是一个集合来求组合的话,就需要startIndex。如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex。
3. 递归+回溯法+剪枝
思路:
1. 确定参数及返回值
需要startIndex来规避重复结果
private void backtracking(int[] candidates, int target, int pathSum, int startIndex)
2. 确定终止条件
判断目前路径的总和是否大于target
if (pathSum >= target) { if (pathSum == target) { result.add(new ArrayList<>(path)); } return; }
3. 确定单层递归的逻辑
startIndex需要包含当前选取的元素,因为每个元素可以重复选取。for循环可以进行剪枝操作,可以提前判断当前层加上下一层的元素是否大于了target,大于了就没必要去下一层了
for (int i = startIndex; i < candidates.length && pathSum + candidates[i] <= target; i++) { path.add(candidates[i]); pathSum += candidates[i]; // 这里startIndex依旧为i,因为candidates每个元素可以重复选取 backtracking(candidates, target, pathSum, i); // 回溯 path.remove(path.size() - 1); pathSum -= candidates[i]; }
List<List<Integer>> result;
List<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
path = new ArrayList<>();
// 先把candidates从小到大排序
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0);
return result;
}
private void backtracking(int[] candidates, int target, int pathSum, int startIndex) {
// 终止条件
if (pathSum >= target) {
if (pathSum == target) {
result.add(new ArrayList<>(path));
}
return;
}
// pathSum + candidates[i] <= target,发现下一层的和不大于target就可以去下一层,
// 大于的话就直接剪枝,因为candidates已经排好序了,后面的数只会更大
for (int i = startIndex; i < candidates.length && pathSum + candidates[i] <= target; i++) {
path.add(candidates[i]);
pathSum += candidates[i];
// 这里startIndex依旧为i,因为candidates每个元素可以重复选取
backtracking(candidates, target, pathSum, i);
// 回溯
path.remove(path.size() - 1);
pathSum -= candidates[i];
}
}
40. 组合总和II
本题开始涉及到一个问题了:去重。注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
题目链接/文章讲解:代码随想录
重点:
1.递归+回溯
2. 树层去重和树枝去重
3. 我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
思路:
递归+回溯法:
1. 确定参数及返回值
需要用used来记录是是否需要树层去重
private void backtracking(int[] candidates, int target, int sum, int startIndex, Boolean[] used)
2. 确定终止条件
if (sum > target) { return; } if (sum == target) { result.add(new ArrayList<>(path)); return; }
3. 确定单层递归的逻辑
首先数组的从小到大排序的。因为我们要进行的是树层去重,也就是说同一层的同一个数字不需要重复取,即使取了也会得到相同的结果,可是不同层的相同数字是可以取的。我们用used来记录,如果前一个相同的数字取过了,证明是不同层的,如果没取过,说明是相同层的。
还有因为数组的排好序的,如果当前数字加上之前的sum以及超出了范围,可以直接剪枝。
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) { // 使用used == false来达到树层去重 if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) { continue; } sum += candidates[i]; path.add(candidates[i]); // used == true说明了是在不同层用的 used[i] = true; backtracking(candidates, target, sum, i + 1, used); sum -= candidates[i]; path.remove(path.size() - 1); used[i] = false; }
List<Integer> path;
List<List<Integer>> result;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
path = new ArrayList<>();
result = new ArrayList<>();
Boolean[] used = new Boolean[candidates.length];
Arrays.fill(used, false);
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0, used);
return result;
}
private void backtracking(int[] candidates, int target, int sum, int startIndex, Boolean[] used) {
// 终止条件
if (sum > target) {
return;
}
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
// 使用used == false来达到树层去重
if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
continue;
}
sum += candidates[i];
path.add(candidates[i]);
// used == true说明了是在不同层用的
used[i] = true;
backtracking(candidates, target, sum, i + 1, used);
sum -= candidates[i];
path.remove(path.size() - 1);
used[i] = false;
}
}
131. 分割回文串
本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。题目链接/文章讲解:代码随想录
重点:
1. 用startIndex来模拟切割线
思路:
递归+回溯:
1. 确定参数及返回值
private void backtracking(String s, int startIndex)
2. 确定终止条件
切割线已经到最末尾了
if (startIndex >= s.length()) { result.add(new ArrayList<>(path)); return; }
3. 确定单层递归的逻辑
判断当前字串是否为回文串,是的话就加入到path中
for (int i = startIndex; i < s.length(); i++) { if (isPalindrome(s, startIndex, i)) { path.add(s.substring(startIndex, i + 1)); } else { continue; } backtracking(s, i + 1); path.remove(path.size() - 1); }
总结:
- 切割问题可以抽象为组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
List<String> path;
List<List<String>> result;
public List<List<String>> partition(String s) {
path = new ArrayList<>();
result = new ArrayList<>();
backtracking(s, 0);
return result;
}
private void backtracking(String s, int startIndex) {
// 终止条件
if (startIndex >= s.length()) {
result.add(new ArrayList<>(path));
return;
}
// 树的当前层
for (int i = startIndex; i < s.length(); i++) {
// 判断当前子串是否为回文串
if (isPalindrome(s, startIndex, i)) {
path.add(s.substring(startIndex, i + 1));
} else {
continue;
}
// 去树的下一层
backtracking(s, i + 1);
// 回溯
path.remove(path.size() - 1);
}
}
private boolean isPalindrome(String s, int start, int end) {
// 双指针判断回文串
while (start < end) {
if (s.charAt(start) != s.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}