39.组合总和
题目链接:39.组合总和
本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex 上的控制
难度指数:😀😐😕
Q:如果集合里面有0,你还重复选取,那不就死循环了吗?
A:不用担心,组合里面的元素都是正整数。
树形结构:
本题用
target来限制树的高度
代码思路:
定义全局变量:
-
二维数组
vector<vector<int>> result; -
一维数组
vector<int> path;
sum 用来统计 path 里面的和;也可以不用 sum,可以用 target 做相应的减操作,最后如果 target = 0,正好说明找到了一个组合,和等于 target。
(这里也可以直接 sum(path) 放进判断语句,本质一样的,也就是少了两行代码而已哈哈哈哈哈)
vector<vector<int>> result;
vector<int> path;
void backtracking(candidates, target, sum, startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
AC代码: (核心代码模式)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); //递归
sum -= candidates[i]; //回溯
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, target, 0, 0);
return result;
}
};
剪枝(优化):
在用回溯法解决类似问题的时候,剪枝通常要在这个 for循环 做文章
对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。
树形结构:
AC代码: (核心代码模式)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
return;
}
//如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); //递归
sum -= candidates[i]; //回溯
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
sort(candidates.begin(), candidates.end()); //需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
40.组合总和Ⅱ
好题
题目链接:40.组合总和Ⅱ
本题开始涉及到一个问题了:去重。
注意题目中给我们的集合是有重复元素的,那么求出来的组合有可能重复,但题目要求不能有重复组合。 (需要去重)
难度指数:😀😐😕
你可能会这么想:我就直接用之前的方式进行搜索,搜索出来若干个组合,这些组合中可能有重复的,然后用 map 或 set 做去重,将去重后的组合输出。
思路没啥问题,但实现起来麻烦,且容易超时。
代码思路:
我们可以在搜索的过程中直接进行去重
(说人话:使用过的元素不再使用,避免重复)要理解这道题的精髓。
- 树层去重
- 树枝去重
可以用回溯算法解决的任何问题都可以抽象成一个树形结构。
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3 ,(方便起见 candidates已经排序了)
想要去重需要先进行排序
对应的这个下标,用过就赋成1,没用过赋成0,可以把它定义成 bool 类型。
树形结构:
🦄🐧🐣去重的关键在 树层去重
一维数组 path :存放符合条件的单个组合(路径)
二维数组 result :存放各个组合的结果集
这里递归函数无须返回值,因此函数返回类型是 void ;存放组合的结果集也都放在全局变量。
Sum 记录单条路径收集的结果和,startIndex 保证组合中不能重复使用相同的元素,
used[] 数组标记元素是否使用过(使用过标记为true)
思考:为什么是
used[i - 1] == 0而不是used[i - 1] == 1?
vector<int> path; //存放符合条件的单个组合
vector<vector<int>> result; //存放组合集合
void backtracking(nums, targetSum, Sum, startIndex, used) {
//终止条件
if (Sum > targetSum) {
return;
}
if (Sum == targetSum) {
result.push_back(path); //放进结果集
return;
}
//单层搜索的逻辑
for (int i = startIndex; i < nums.size(); i++) { //取1 取1 取2
//树层上要进行去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) { //如果当前遍历元素和前一个元素相同 (i>0是为了防止后面的i-1数组越界)
continue; //就是说这里不取1了,继续往后取 (这就是去重)
}
path.push_back(nums[i]); //收集单个组合
Sum += nums[i];
used[i] = true;
backtracking(nums, targetSum, Sum, i + 1, used); //递归
path.pop_back(); //回溯
Sum -= nums[i]; //回溯
used[i] = false; //回溯
}
}
注:力扣上主函数在调用这个函数之前要先对nums[] 这个集合进行排序。
⚠️去重操作是在排完序之后进行去重的!
AC代码: (核心代码模式)
class Solution {
private:
vector<int> path; //存放符合条件的单个组合
vector<vector<int>> result; //存放组合集合
void backtracking(vector<int>& nums, int targetSum, int Sum, int startIndex, vector<bool>& used) {
//终止条件
if (Sum > targetSum) {
return;
}
if (Sum == targetSum) {
result.push_back(path); //放进结果集
return;
}
//单层搜索的逻辑
for (int i = startIndex; i < nums.size(); i++) { //取1 取1 取2
//树层上要进行去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {
continue;
}
path.push_back(nums[i]); //收集单个组合
Sum += nums[i];
used[i] = true;
backtracking(nums, targetSum, Sum, i + 1, used); //递归
path.pop_back();
Sum -= nums[i];
used[i] = false;
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& nums, int targetSum) {
int Sum, startIndex;
vector<bool> used(nums.size(), false);
path.clear();
result.clear();
//先对nums进行排序,让其相同的元素都挨在一起
sort(nums.begin(), nums.end());
backtracking(nums, targetSum, 0, 0, used);
return result;
}
};
131.分割回文串
切割问题其实是一种组合问题!
题目链接:131.分割回文串
难度指数:😀😐😕😫
代码思路:
一维数组 path :存放符合条件的单个组合(路径)
二维数组 result :存放组合集合
这两个全局变量,我们也是可以考虑放到递归的参数里面。
startIndex 保证切割过的不会重复切割; startIndex 就是切割线(的位置)。
⚠️C++er,注意传入递归函数的参数是const,还是引用之类的。
if (startIndex == s.size()) { //说明已经到了叶子节点
result.push_back(path);
return;
}
↑↑↑ 同学疑问:收集的
path应该得是符合回文子串规则,才能加入到result[]数组,怎么能直接就把path放进result?其实我们是将判断回文的逻辑放到单层搜索的逻辑里面
在递归函数里面的for循环中,如何表示切割的子串? 这是一个难点
答:startIndex 是切割线,那么切割的字串就是 [startIndex, i] 这个左闭右闭的区间。
i每向后移动一位,就代表一个子串,再向后移动,就代表一个子串,这样startIndex到i之间就是递归搜索过程中不断搜索的范围。
知道了子串的范围之后,就要判断它是不是回文。
vector<int> path; //存放符合条件的单个组合(路径)
vector<vector<int>> result; //存放组合集合
void backtracking(const string& s, int startIndex) {
//终止条件
if (startIndex >= s.size()) { //说明已经到了叶子节点 (其实 == 就已经算切割到末尾了)
result.push_back(path);
return;
}
//单层搜索的逻辑
for (int i = startIndex; i < s.size(); i++) {
//(判断切割的子串是不是回文串,若是,放到path数组里)
if (isPalindrome(s, startIndex, i)) { //如果是回文
path.push_back(子串);
}
else { //不是回文,for循环进入下一个i
continue;
}
backtracking(s, i + 1); //递归
path.pop_back(); //回溯
return;
}
}
AC代码: (核心代码模式)
class Solution {
private:
vector<string> path; //存放符合条件的单个组合(路径)
vector<vector<string>> result; //存放组合集合
void backtracking(const string& s, int startIndex) {
//终止条件
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 { //若不是回文 (for循环进入下一个i)
continue;
}
backtracking(s, i + 1); //递归
path.pop_back(); //回溯
//return; (不要return)
}
}
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;
}
};