回溯法理论
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯。
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
回溯法解决的问题都可以抽象为树形结构,
回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
回溯是递归的纵横拓展,主要是递归(纵)+局部暴力枚举(横)。
**为什么要撤销:**因为回溯本质上仍然是枚举,你不喜欢她的前任,你要将她前任的所有东西都仍然,然后才愿意重新开始!
剪枝就是去掉那些不必要的递归,从而提高执行效率。
回溯法模版
1、回溯函数返回值以及参数
回溯算法中函数返回值一般为void。
回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
2、回溯函数终止条件
树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
3、回溯搜索的遍历过程
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
4、回溯算法模版的框架
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
溯法就用递归来解决嵌套层数的问题。
可选择的范围随着选择的进行而收缩,调整可选择的范围。使用startlindex来记录下一层递归,搜索的起始位置
class Solution {
public:
//通过回溯法来实现
//将组合问题看做是一个树,树的高度就是迭代的次数,树的宽度就是for循环的次数;
//树的纵向遍历是采用的递归,下一层的开始搜索要从i+1开始,因为本层是i;
//每成功到达一次叶子结点,就回溯一次,撤销处理的节点。然后开始下一次for循环;
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex)
{
if(path.size() == k)
{
result.push_back(path);
return;
}
for(int i = startIndex; i <= n;i++)
{
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};
剪枝操作:
class Solution {
public:
//通过回溯法来实现
//将组合问题看做是一个树,树的高度就是迭代的次数,树的宽度就是for循环的次数;
//树的纵向遍历是采用的递归,下一层的开始搜索要从i+1开始,因为本层是i;
//每成功到达一次叶子结点,就回溯一次,撤销处理的节点。然后开始下一次for循环;
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex)
{
if(path.size() == k)
{
result.push_back(path);
return;
}
//剪枝操作:已经选择的元素个数:path.size(); 还需要选择的元素个数:K - path.size(); 在集合中最多要从开始遍历的位置:n - (k - path.size()) + 1;//+1是因为包括起始位置
//也就是说,如果当前元素后面没有K - path.size()个元素就不需要再遍历了,那么当前元素最大就是n - (k - path.size()) + 1,再大后面就没有那么多元素了
for(int i = startIndex; i <= n - (k - path.size()) + 1;i++)
{
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};
组合III
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex)
{
if(path.size() == k)
{
if(sum == targetSum) result.push_back(path);
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
for(int i = startIndex; i <= 9; i++)
{
sum += i;//处理
path.push_back(i);//处理
backtracking(targetSum, k, sum, i + 1);
sum -= i;//撤销处理
path.pop_back();//撤销处理
}
}
vector<vector<int>> combinationSum3(int k, int n) {
result.clear();
path.clear();
backtracking(n, k, 0, 1);
return result;
}
};
剪枝操作:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex)
{
if(path.size() == k)
{
if(sum == targetSum) result.push_back(path);
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
for(int i = startIndex; i <= 9; i++)
{
sum += i;//处理
path.push_back(i);//处理
if (sum > targetSum) { // 剪枝操作
sum -= i; // 剪枝之前先把回溯做了
path.pop_back(); // 剪枝之前先把回溯做了
return;
}
backtracking(targetSum, k, sum, i + 1);
sum -= i;//撤销处理
path.pop_back();//撤销处理
}
}
vector<vector<int>> combinationSum3(int k, int n) {
result.clear();
path.clear();
backtracking(n, k, 0, 1);
return result;
}
};
再减枝
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex)
{
if(path.size() == k)
{
if(sum == targetSum) result.push_back(path);
return;// 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) // 剪枝
{
sum += i;//处理
path.push_back(i);//处理
if (sum > targetSum) { // 剪枝操作
sum -= i; // 剪枝之前先把回溯做了
path.pop_back(); // 剪枝之前先把回溯做了
return;
}
backtracking(targetSum, k, sum, i + 1);
sum -= i;//撤销处理
path.pop_back();//撤销处理
}
}
vector<vector<int>> combinationSum3(int k, int n) {
result.clear();
path.clear();
backtracking(n, k, 0, 1);
return result;
}
};
电话号码的字母组合
数字和字母如何映射:
可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射
回溯法来解决n个for循环的问题
class Solution {
private:
const string letterMap[10] =
{
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
};
public:
vector<string> result;
string s;
void backtracking(const string& digits, int index)
{
if(index == digits.size())//遍历到第n个数字
{
result.push_back(s);
return;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for(int i = 0; i < letters.size();i++)//每次都是从0开始,因为每个数字对应的字符集是不一样的;
{
s.push_back(letters[i]);
backtracking(digits, index + 1);//对应下一个字符集
s.pop_back();
}
}
vector<string> letterCombinations(string digits) {
s.clear();
result.clear();
if(digits.size() == 0)
{
return result;
}
backtracking(digits, 0);
return result;
}
};
组合总和
class Solution {
private:
//如果是一个集合来求组合的话,就需要startIndex
// 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
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);//本体元素可以重复选择,所以这里传入的是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;
}
};
剪枝操作:
1、当本次迭代中的值加上累计的值大于target时不再进入递归函数;
2、实现1要进行排序,防止后面的值出现满足加上累计的值小于target的情况
在求和问题中,排序之后加剪枝是常见的套路!
class Solution {
private:
//如果是一个集合来求组合的话,就需要startIndex
// 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
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() && sum + candidates[i] <= target; i++)
{
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i);//本体元素可以重复选择,所以这里传入的是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;
}
};
组合总和III
当某一个枝,当user[i - 1] == false时,说明
used[i - 1] == false:前一个元素在上层已经被回溯弹出(在同一树层中使用过)。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
if (sum == target) {
result.push_back(path);
return;
}
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();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
path.clear();
result.clear();
// 首先把给candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
分割回文串
class Solution {
private:
vector<vector<string>> result;
vector<string> path;
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))
{
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
}
else
{
continue;
}
backtracking(s, 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;
}
};