39. 组合总和
分析
第一眼做这个以为跟前面的都一样,无脑三部曲.但后来才发现题给的解释说,集合中的数可以使用多次. 傻眼了. 那怎么去考虑终止条件呢. 还有如何去深度搜索和宽度搜索呢?
注意叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
思路
回溯三部曲
- 递归函数参数
二维数组result存放结果集,数组path存放符合条件的结果。
参数:
集合
candiates
题目给的数组
target
目标和
sum
总和
startIndex
startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startInde
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
ector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
- 递归终止条件
根据题目, 终止条件只有两种: sum大于target和sum等于target。
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
- 单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合。
如何重复选取?
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
sum -= candidates[i]; // 回溯
path.pop_back(); // 回溯
}
不用i+1了,表示可以重复读取当前的数
相当于下一层递归循环 , i 还是从这一层的startIndex开始, 这样就实现可重复读取啦
Code
C++
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
int sum = 0;
void backtracking(vector<int>& candidates,int startIndex, int target, int sum){
if(sum == target){
res.push_back(path);
return;
}
if(sum > target){
return;
}
// 每层遍历
for(int i = startIndex; i < candidates.size(); i++){
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, i, target, sum);
//回溯
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, 0, target, 0);
return res;
}
};
Java
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public void backtracking(int[] candidates, int target, int startIndex, int sum){
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
if(sum > target){
return;
}
// 单层遍历
for(int i = startIndex; i < candidates.length; i++){
path.add(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, i, sum);
sum -= candidates[i];
path.remove(path.size()-1);
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0, 0);
return res;
}
}
剪枝优化
对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。
可以在for循环判断的终止条件中增加限制:
如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
所以单层for
搜索可以改为 sum+= candidates原本是在for循环里面 .在这里判断, 就会少走下面的递归函数, 完成剪枝的目的
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
40. 组合总和 II
分析
跟上一题不同的是, 本题candidates 中的每个数字在每个组合中只能使用一次。
本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates
所谓去重,其实就是使用过的元素不能重复选取
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
思路
回溯三部曲
- 递归函数参数
加一个used[]
数组, 用来记录同一树枝上的元素是否使用过
用来记录同一数层上的元素是否使用过
vector<vector<int>> result; // 存放组合集合
vector<int> path; // 符合条件的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
- 递归终止条件
跟上一题一样的
if (sum > target) { // 这个条件其实可以省略
return;
}
if (sum == target) {
result.push_back(path);
return;
}
- 单层搜索逻辑
这里与上一题最大的区别就是去重啦,一定一定!!要在开始先排序
难点在于如何判断同一树层上的元素是否使用过
用used
数组, 0 代表没用过, 1 代表已经用过
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
为什么 used[i - 1] == false 就是同一树层呢?
因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
Code
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
int sum = 0;
public void backtracking(int[] candidates, int target, int startIndex, int sum, boolean[] used){
if(sum == target){
res.add(new ArrayList<>(path));
return;
}
if(sum > target){
return;
}
// 单层遍历
for(int i = startIndex; i < candidates.length ; i++){
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if(i > 0&& candidates[i] == candidates[i-1] && used[i - 1] == false) continue;
used[i] = true;
path.add(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, i+1, sum, used);
sum -= candidates[i];
path.remove(path.size()-1);
used[i] = false;
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
// 加标志数组,用来辅助判断同层节点是否已经遍历
Arrays.fill(used, false);
// 为了将重复的数字都放到一起,所以先进行排序
Arrays.sort(candidates);
backtracking(candidates, target, 0, 0, used);
return res;
}
}
语法小补充
vector<bool> used(candidates.size(), false);
在java中,可以这么实现
Arrays.fill(used, false);
131. 分割回文串
分析
两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
切割问题类似于组合问题
- 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
- 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
递归纵向逐个切割,
for循环横向遍历切割
思路
回溯三部曲
- 递归函数参数
path存放切割后的回文子串
二维数组res存放结果集
startIndex , 这里就一个集合,所以也要使用
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
- 递归函数终止条件
这个startIndex就可以看做是一个切割线
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
}
- 单层搜索逻辑
主要是在递归循环中如何取子串
startIndex => i
就是子串
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 如果不是则直接跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经添加的子串
}
因为不能切割在同一处, 遍历到下一层的时候, i+1
- 判断回文字符串
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
Code
class Solution {
private:
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 不是回文,跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经添加的子串
}
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s, 0);
return result;
}
};