39. 组合总和
思路:回溯法,将问题抽象为树形结构,使用回溯法解决。
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
而在77.组合和216.组合总和III中都可以知道要递归K层,因为要取k个元素的组合。
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0, 0);
return res;
}
private void backtracking(int[] candidates, int target, int sum, int start) {
if (sum > target) {
// 超过目标和,直接结束
return;
}
if (sum == target) {
// 找到目标和
res.add(new ArrayList<>(path));
return;
}
// 回溯算法框架
for (int i = start; i < candidates.length; i++) {
path.add(candidates[i]);
sum += candidates[i];
// 关键点: 不用i+1了,表示可以重复读取当前的数
backtracking(candidates, target, sum, i);
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
40. 组合总和II
思路:本题与39题大体思路一致,但是本题中存在重复数据,重点在于去重操作。注意去重的时候有两种维度,一种是在同一个树枝上,一种是在同一个树层上,本题需要对同一个树层上使用过的元素进行去重。在同一个树层上进行去重,需要对数组进行排序。
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 将数组排序,相同的元素放在一起,方便去重
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0);
return res;
}
private void backtracking(int[] candidates, int target, int sum, int start) {
// base case,超过目标和,直接结束
if (sum > target) return;
// base case,达到目标和,找到符合条件的组合
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
// 要对同一树层使用过的元素进行跳过
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
path.add(candidates[i]);
sum += candidates[i];
// candidates 中的每个数字在每个组合中只能使用 一次 。
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
131. 分割回文串
思路:将问题抽象成树形结构解决,使用回溯法。但要注意,在代码中,递归需要传入的参数start就是字符串的切割线,在单层逻辑的处理中,[start, i] 就是我们要截取的子串。
递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。
class Solution {
private List<List<String>> res = new ArrayList<>();
private List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
backtracking(s, 0);
return res;
}
// 回溯算法框架
private void backtracking(String s, int start) {
if (start == s.length()) {
// base case,走到叶子节点
// 即整个 s 被成功分割为若干个回文子串,记下答案
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < s.length(); i++) {
if (!isPalindrome(s, start, i)) {
// s[start..i] 不是回文串,不能分割
continue;
}
// s[start..i] 是一个回文串,可以进行分割
// 做选择,把 s[start..i] 放入路径列表中
path.add(s.substring(start, i + 1));
// 进入回溯树的下一层,继续切分 s[i+1..]
backtracking(s, i + 1);
// 撤销选择
path.remove(path.size() - 1);
}
}
// 用双指针技巧判断 s[lo..hi] 是否是一个回文串
private boolean isPalindrome(String s, int lo, int hi) {
while (lo < hi) {
if (s.charAt(lo) != s.charAt(hi)) {
return false;
}
lo++;
hi--;
}
return true;
}
}