定义:回溯法也可以叫做回溯搜索法,是一种搜索的方式。有递归就会有回溯,回溯函数也就是递归函数。
回溯法的效率:回溯的本质是穷举,穷举所有可能,然后选出想要的答案。剪枝操作可以让回溯法高效一些,但回溯法仍然是穷举。
一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
注意:组合是不强调元素顺序的,排列是强调元素顺序。组合无序,排列有序。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
先写逻辑,再确定递归函数参数:
n,题目中的n。
k,题目中的k,集合个数。
startindex,下一层for循环搜索的起始位置。
终止条件:
k就是树的深度,只取k个元素,path.size() =k就终止。
单层搜索:
path收集每次选取的元素,相当于树型结构里的边。
全局变量:
一维数组path来存放符合条件的结果,二维数组result来存放结果集。
class Solution {
public:
vector<vector<int>> result;//二维数组,存放符合条件结果的集合
vector<int> path;//一维数组,存放符合条件的一个结果 也是存放路径
//参数就是树的宽度n,深度k,startindex是为了控制单次递归时从哪里开始遍历,防止出现重复的组合
//如,一个结果集合是[1, 2] [1, 3] [1, 4];那么下一个结果集合遍历的开始位置就是2开始,即[2, 3] [2, 4]
void backtracking(int n, int k, int startindex)
{
//遍历到树的叶子节点,也就是走过了两个节点,即path的大小为k时,回溯终止
//相当于 一维数组path大小=k,找到了一个子集大小为k的组合,需要在result中保存path,也就是保存了根节点到叶子节点的路径,然后终止回溯
if(path.size()==k)
{
result.push_back(path);
return;
}
//单层递归
for(int i = startindex;i<=n;i++)//控制树的横向遍历,i为本次搜索的起始位置
{
path.push_back(i);//处理节点
backtracking(n, k, i+1);//递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back();//回溯,撤销处理的节点
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
优化过程如下:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
+1是因为包括起始位置,要的是一个左闭的集合
把上面代码的for(int i = startindex;i<=n;i++)
替换成for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
先写逻辑,再确定递归函数参数:
目标和targetsum,题目中的n。
k,题目中的k,集合个数。
sum,每条路径累加和,path里元素总和。
startindex,下一层for循环搜索的起始位置。
终止条件:
k就是树的深度,只取k个元素,path.size() =k就终止。
结合题目,如果此时path里收集到的元素和sum和题目要求的和n,即目标和targetSum相同,就用result收集当前的结果。
单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。
全局变量:
一维数组path来存放符合条件的结果,二维数组result来存放结果集。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int k, int targetsum, int sum, int startindex)
{
//终止条件
if(path.size()==k)
{
if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
return;//搜索到叶子节点
}
//单层搜索
//数字只能在1-9之间
for(int i = startindex; i<=9; i++)
{
sum += i;//累加求和
path.push_back(i);//存放组合
backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
sum -= i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
如果第二次取元素值大于n,即目标和,后续操作就没有意义了,直接返回,做剪枝处理if (sum > targetSum) return;
,有三种方式:
i <= 9-(k-path.size())+1
方式1:回溯开始就剪枝
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int k, int targetsum, int sum, int startindex)
{
//剪枝1 回溯开始就剪枝
if(sum>targetsum) return;
//终止条件
if(path.size()==k)
{
if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
return;//搜索到叶子节点
}
//单层搜索
//数字只能在1-9之间
for(int i = startindex; i<=9; i++)
{
sum += i;//累加求和
path.push_back(i);//存放组合
backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
sum -= i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
方式2:调用递归函数开始前剪枝,先回溯,再剪枝
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int k, int targetsum, int sum, int startindex)
{
//终止条件
if(path.size()==k)
{
if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
return;//搜索到叶子节点
}
//剪枝2 调用递归前剪枝 要先回溯再剪枝
for(int i = startindex; i<=9; i++)
{
sum += i;//累加求和
path.push_back(i);//存放组合
if(sum > targetsum)//剪枝
{
sum -= i;//剪枝之前先回溯
path.pop_back();//剪枝之前先回溯
return;
}
backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
sum -= i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
方式3:for循环控制条件剪枝
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int k, int targetsum, int sum, int startindex)
{
//终止条件
if(path.size()==k)
{
if(sum==targetsum) result.push_back(path);//如果累加和与目标和相同,存放结果
return;//搜索到叶子节点
}
//剪枝3
for(int i = startindex; i<=9 - (k-path.size()) + 1; i++)
{
sum += i;//累加求和
path.push_back(i);//存放组合
backtracking(k, targetsum, sum, i+1);//startindex调整为i+1
sum -= i;//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 0, 1);
return result;
}
};
先写逻辑,再确定递归函数参数:
digits,题目中的digits,数字串。
index,遍历数字串digits,记录遍历的第几个数字,也表示树的深度
终止条件:
当数字串遍历完就终止,即index==digits.size()
。存放当前层的结果,再结束递归
单层搜索:
首先要用index获取数字串的数字
然后找到该数字对应的字母集
最后通过for循环处理这个字母集:s存放每次在字母集选取的元素,然后递归,再回溯
全局变量:
一个字符串s收集叶子节点结果
一个字符串数组result存放结果集
class Solution {
//私有属性 二维数组 数字与字母映射
private:
const string letterMap[10] =
{
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz", //9
};
public:
string s;//保存符合条件的一个结果
vector<string> result;//保存结果集
//参数:1.题目给的字符串digits 引用传入 const修饰;2.当前数字,字符串当前元素
void backtracking(const string& digits, int index)
{
//终止条件 如果字符串遍历完就终止
if(index==digits.size())
{
result.push_back(s);
return;
}
//单层搜索 二维数组
int digit = digits[index] - '0';//index指向的数字由string转成int
string letters = letterMap[digit];//获取index指向的数字对应的字母
for(int i=0; i<letters.size();i++)
{
s.push_back(letters[i]);//存放字母
backtracking(digits, index+1);//递归,index+1表示处理下一个数字
s.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return result;
backtracking(digits, 0);
return result;
}
};
class Solution {
//私有属性 map 数字与字母映射
private:
const unordered_map<char, string> phoneMap{
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
public:
string s;//保存符合条件的一个结果
vector<string> result;//保存结果集
//参数:1.题目给的字符串digits 引用传入 const修饰;2.当前数字,字符串当前元素
void backtracking(const string& digits, int index)
{
//终止条件 如果字符串遍历完就终止
if(index==digits.size())
{
result.push_back(s);
return;
}
//单层搜索 map
char digit = digits[index];//获取当前遍历的数字
string letters = phoneMap.at(digit);//获取当前遍历数字 对应的 字母集 注意访问方式
for(int i=0; i<letters.size();i++)
{
s.push_back(letters[i]);//存放字母
backtracking(digits, index+1);//递归,index+1表示处理下一个数字
s.pop_back();//回溯
}
}
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return result;
backtracking(digits, 0);
return result;
}
};
先写逻辑,再确定递归函数参数:
candidates,集合
target,目标值
sum,每条路径累加和,path里元素总和。也可以用target做减法,target=0就说明找到符合的结果了。
startindex,下一层for循环搜索的起始位置。
终止条件:
sum大于target和sum等于target
单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。
全局变量:
一维数组path来存放符合条件的结果,二维数组result来存放结果集。
本题和77.组合、216.组合总和Ⅲ不同的是
class Solution {
public:
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+1
sum -= candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0, 0);
return result;
}
};
sum大于target时,还是进入了下一层递归,但其实可以直接返回,不需要进入下一层递归。
因此,sum大于target时再for循环所有范围进行剪枝。对总集合排序,如果下一层的sum,即本层的sum+candidates[i]大于target时,就可以结束本轮for循环的遍历。
class Solution {
public:
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+1
sum -= candidates[i];//回溯
path.pop_back();//回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 需要排序 剪枝前需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
和39.组合总和的不同点
和39.组合总和的共同点:
最后本题和39.组合总和要求一样,解集不能包含重复的组合。
题目难点:
在于区别2中,集合(数组candidates)有重复元素,但还不能有重复的组合。元素在同一个组合内是可以重复的,但两个组合不能相同。
因此,去重操作就是要去重的是同一树层上的使用过,同一树枝上的都是一个组合里的元素,不用去重。树层去重的话,需要对数组排序
先写逻辑,再确定递归函数参数:
candidates,集合
target,目标值
sum,每条路径累加和,path里元素总和。也可以用target做减法,target=0就说明找到符合的结果了。
startindex,下一层for循环搜索的起始位置。
used,bool型数组,用来记录同一树枝上的元素是否使用过,用于集合去重
终止条件:
和39.组合总和一样,sum大于target和sum等于target,同样的,剪枝操作时可以省掉sum大于target条件了。
单层搜索:
path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。
全局变量:一维数组path来存放符合条件的结果,二维数组result来存放结果集。
去重方式1-使用标记数组
[i] == candidates[i - 1]时:
去重方式2-使用下标索引: 排序之后,相同元素会挨在一起。去重时,从i的下一个位置取元素,判断前后元素是否相同,相同则跳过
去重方式3-使用set去重: set记录哪些元素使用过,对同一父节点下同一层去重
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
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++)
{
//判断当前值在同一树层是否使用过 同一树层元素不能重复,candidates[i] == candidates[i - 1]时,有
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
//candidates[i]==candidates[i-1]看是否为同一元素, used[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,i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);//bool型数组 判断元素在树层还是树枝
// 先candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
// 索引下标 不使用数组
void backtracking(vector<int>& candidates, int target, int sum, int startindex)
{
//终止条件
if(sum==target)
{
result.push_back(path);
return;
}
//单层搜索
for(int i=startindex; i<candidates.size() && sum+candidates[i] <= target; i++)//剪枝
{
//去重 从i下一个位置取元素 前后元素相同则跳过
if(i>startindex && candidates[i]==candidates[i-1]) continue;
//操作
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i+1);//每个数字在每个组合中只能使用一次
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return result;
}
};
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
//不使用sum
void backtracking(vector<int>& candidates, int target, int startindex)
{
//终止条件
if(target==0)
{
result.push_back(path);
return;
}
//单层搜索
for(int i=startindex; i<candidates.size() && target-candidates[i] >= 0; i++)//剪枝
{
//去重 从i下一个位置取元素 前后元素相同则跳过
if(i>startindex && candidates[i]==candidates[i-1]) continue;
//操作
target -= candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i+1);//每个数字在每个组合中只能使用一次
target += candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0);
return result;
}
};
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
//3.set去重+不使用sum
void backtracking(vector<int>& candidates, int target, int startindex)
{
if(target==0)
{
result.push_back(path);
return;
}
unordered_set<int> uset;
for(int i=startindex; i<candidates.size() && target-candidates[i]>=0; i++)
{
if(uset.find(candidates[i]) != uset.end()) continue;
uset.insert(candidates[i]);
target -= candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i+1);
target += candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return result;
}
};
剪枝优化和39题的一样
//先写逻辑再确定参数
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77.组合:
216.组合总和Ⅲ:
17.电话号码的字母组合:
39.组合总和:
40.组合总和Ⅱ:
对于组合问题,什么时候需要startIndex?