1.回溯算法
回溯三要素
- 回溯函数模板返回值以及参数
回溯算法中函数返回值一般为void。
再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
但后面的回溯题目的讲解中,为了方便大家理解,我在一开始就帮大家把参数确定下来。
回溯函数伪代码如下:
void backtracking(参数)
- 回溯函数终止条件
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
所以回溯函数终止条件伪代码如下:
if (终止条件) {
存放结果;
return;
}
- 回溯搜索的遍历过程
在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
如图:
回溯函数遍历过程伪代码如下:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
backtracking这里自己调用自己,实现递归。
for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
分析完过程,回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
2.组合问题
题目链接:. - 力扣(LeetCode)
讲解链接:代码随想录 (programmercarl.com)
视频链接:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili
若干数的组合,是经典的回溯问题。
1.参数与返回值:无返回值,参数传入三个int类型,分别代表数字总数、选用数字数、开始位置(每个数字只能用一次)。同时需要两个数组,存放数字组合和结果集。
2.终止条件:当组合的大小到达k值时终止,并且将当前组合存放到结果集中。
3.回溯过程:for循环是纵向遍历,即第一个数字取哪个。同时要从startIndex开始遍历,每次纵向遍历就将startIndex+1,避免了数字的重复使用。循环中首先将当前数字加入组合,然后递归纵向加入其他数字,接着进行回溯,将当前数字弹出。
减枝过程:当所需元素个数大于剩余遍历元素数目时,就不需要继续遍历了,因此修改for循环的范围来减少不必要运算。
代码入下:
class Solution {
private:
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 - (k - path.size()) + 1; ++i) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
3.组合总和
题目链接:. - 力扣(LeetCode)
讲解链接:代码随想录 (programmercarl.com)
视频链接:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III_哔哩哔哩_bilibili
1.参数与返回值:无返回值,参数除了元素目标数量与元素目标和之外,还需要记录当前选用元素的和以及开始位置。同样需要两个数组记录组合与结果集。
2.终止条件:当组合的大小到达k时终止,如果其和满足要求就将当前组合存放到结果集中,否则直接返回。
3.回溯过程:for循环横向遍历,同样从startIndex开始。循环中将当前元素加入组合中,接着递归开始位置+1,同样回溯弹出元素,期间当前元素和同步变化。
剪枝:当前元素和大于目标和时就不用继续往下遍历了,循环中加入元素后每次判断当前和与目标的大小,如果超过立刻回溯返回。
代码如下:
class Solution {
private:
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;
}
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();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 0, 1);
return result;
}
};
4.电话号码字母组合
题目链接:. - 力扣(LeetCode)
讲解链接:代码随想录 (programmercarl.com)
视频链接:还得用回溯算法!| LeetCode:17.电话号码的字母组合_哔哩哔哩_bilibili
本题需要一个字符串数组记录每个按键对应的字母。与组合问题相同,横向遍历第一个数字所对应的字母,纵向遍历接下来的字母,只是每次遍历的集合不同。
首先输入数字以字符串存储,每次循环要先将字符转化为数字,通过ch-'0'转换,然后通过之前定义的字符串数组取得对应的遍历集合。
1.参数与返回值:无返回值,参数传入所输入字符串,和目前遍历的数字序号。
2.终止条件:当数字序号与字符串的大小相等时,即几个输入数字都遍历完了,将组合加入结果集。
3.回溯过程:for循环中,进行上述的预处理操作,加入当前元素,然后递归并将数字序号+1,接着回溯弹出。每次都从数字对应的集合遍历。
代码入下:
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
};
vector<string> result;
string s;
void backtracking(const string& digits, int index){
if (index == digits.size()){
result.push_back(s);
return;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for (int i = 0; i < letters.size(); ++i) {
s.push_back(letters[i]);
backtracking(digits, index + 1);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
backtracking(digits, 0);
return result;
}
}