LeetCode 39 组合总和
思路
组合中可重复选取同一个数字。为了方便剪枝,可以对候选数字排序成递增。
考虑剪枝:
- 当路径和已经大于目标和,由于候选序列递增,同深度下后方数字都可以舍弃,更大深度下也可舍弃
- 为了不重复生成组合,每次选择的新数字从路径最后一个数字开始。由于数值需要对应索引,传参时需要上一个数字的索引
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:candidates候选数字,target目标和,start开始选择数字的索引 返回值:空 全局变量:path选择路径,sum路径和,result结果集
- 回溯函数的结束条件 sum等于target时,path加入result,返回
- 回溯搜索的遍历过程
从start索引开始逐个选择候选集中的数字
- candidates[i]加入path,更新sum
- 如果sum大于target,还原path和sum,退出循环
- 递归调用回溯函数,start参数为i
- 还原path和sum
解法
class Solution {
List<List<Integer>> result;
List<Integer> path;
int sum;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
result = new ArrayList<>();
path = new ArrayList<>();
sum = 0;
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
public void backTracking(int[] candidates, int target, int start) {
if (sum == target) {
result.add(new ArrayList<>(path));
return ;
}
for (int i = start; i < candidates.length; i++) {
sum += candidates[i];
if (sum > target) {
sum -= candidates[i];
break ;
}
path.add(candidates[i]);
backTracking(candidates, target, i);
path.remove(path.size()-1);
sum -= candidates[i];
}
}
}
LeetCode 40 组合总和II
思路
candidates中会有数字重复,且其中的数字只能使用一次。 根据上一题的思路,我们可以先对数组排序。
考虑剪枝:
- 当路径和已经大于目标和,由于候选序列非递减,同深度下后方数字都可以舍弃,更大深度下也可舍弃
考虑去重:
- 为了不重复使用一个数字,每次选择的新数字从索引i+1开始。
- 数组排序后相同的数字相邻,为了防止相同次数使用相同的数字,考虑与前一个数字相同的candidates[i],仅当candidates[i-1]在路径中,才可以使用candidates[i](可以理解为相同的数字只能连续使用,这样对于出现次数k,只可能取前k个此数字)。为了判断每个数字是否存在路径中,增设数组inPath,长度等于candidates,1标识在路径中,0标识不在
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:candidates候选数字,target目标和,start开始选择数字的索引 返回值:空 全局变量:path选择路径,sum路径和,result结果集
- 回溯函数的结束条件 sum等于target时,path加入result,返回
- 回溯搜索的遍历过程
从start索引开始逐个选择候选集中的数字
- 如果不符合去重2️⃣,跳过本轮循环
- candidates[i]加入path,更新sum
- 如果sum大于target,还原path和sum,退出循环
- 递归调用回溯函数,start参数为i
- 还原path和sum
解法
class Solution {
List<List<Integer>> result;
List<Integer> path;
int sum;
int[] inPath;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
result = new ArrayList<>();
path = new ArrayList<>();
sum = 0;
inPath = new int[candidates.length];
Arrays.sort(candidates);
backTracking(candidates, target, 0);
return result;
}
public void backTracking(int[] candidates, int target, int start) {
if (sum == target) {
result.add(new ArrayList<>(path));
return ;
}
for (int i = start; i < candidates.length; i++) {
if (i > 0 && candidates[i] == candidates[i-1] && inPath[i-1] == 0) {
continue;
}
sum += candidates[i];
if (sum > target) {
sum -= candidates[i];
break;
}
path.add(candidates[i]);
inPath[i] = 1;
backTracking(candidates, target, i+1);
inPath[i] = 0;
path.remove(path.size()-1);
sum -= candidates[i];
}
}
}
LeetCode 131 分割回文串
思路
考虑如何判断回文:
- 可以每次用双指针法左右相互比较
- 给定一个字符串
s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]且s[1:n-1]是回文字串。故可以在一开始计算s的任意子串是否为回文串(动态规划),回溯时只需要查询
考虑回溯三要素:
- 回溯函数的参数和返回值 参数:串s,下一个字串的起始索引start 返回值:空 全局变量:结果集result,path切割路径
- 回溯函数的结束条件 start大于等于s长度,path加入结果集
- 回溯搜索的遍历过程
从start+1开始的每个切割位置i
- 判断子串[start, i]是否是回文串(左闭右开),不是就跳过本次循环
- 把子串加入path
- 递归调用回溯函数,start参数为i
- 还原path
解法
class Solution {
boolean[][] isPalindrome;
List<List<String>> result;
List<String> path;
public List<List<String>> partition(String s) {
isPalindrome = computePalindrome(s);
result = new ArrayList<>();
path = new ArrayList<>();
backTracking(s, 0);
return result;
}
public void backTracking(String s, int start) {
if (start == s.length()) {
result.add(new ArrayList<>(path));
return ;
}
for (int i = start+1; i <= s.length(); i++) {
if (!isPalindrome[start][i-1]) {
continue;
}
path.add(s.substring(start, i));
backTracking(s, i);
path.remove(path.size()-1);
}
}
private boolean[][] computePalindrome(String s) {
boolean[][] res = new boolean[s.length()][s.length()];
for (int i = res.length-1; i >= 0; i--) {
for (int j = i; j < res.length; j++) {
if (i == j) {
res[i][j] = true;
}
else if (j == i + 1) {
res[i][j] = (s.charAt(i) == s.charAt(j));
}
else {
res[i][j] = res[i+1][j-1] && (s.charAt(i) == s.charAt(j));
}
}
}
return res;
}
}
今日收获总结
今日学习2.5小时,用回溯思想暴力搜索可以把问题解决过程抽象为树结构