回溯(每周更新)
理论
题目分类
- 组合: N个数里面按一定规则找出k个数的集合
- 分割: 一个字符串按一定规则几种分割方式
- 子集: 一个N个数的集合里有多少符合条件的子集
- 排序: N个数按一定规则全排列,有几种排列方式
- 棋盘: N皇后,解数独
什么是回溯法?
回溯法又叫做回溯搜索法
回溯是递归的副产品, 只要有递归就会有回溯
回溯搜索模版
三部曲
1. 终止条件
2. 处理节点
3. 回溯
终止条件
if (终止条件) {
存放结果;
return;
}
处理节点 + 回溯
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
完整
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
看到这里,你可能没有办法理解, 不用怕, 我下面用例题进行举例分析并且进行优化( 剪枝 )
题目
组合问题
分析
可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。
第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。
图中可以发现n相当于树的宽度,k相当于树的深度。
代码
- 定义全局变量 存储 结果
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
- 定义回溯函数
void backtravel(int n,int k, int startIndex)
- 回溯函数终止条件
什么时候到达所谓的叶子节点了呢?
path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
如图红色部分:
if (path.size() == k) {
result.push_back(path);
return;
}
- 单层搜索的过程
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
path.push_back(i); // 处理节点
backtravel(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
完整code
class Solution {
public:
// 定义一个存储结果的二维数组
vector<vector<int>> result;
vector<int> path; // 存储一种组合
// 没有返回值
void backtravel(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); // 添加值
backtravel(n, k, i+1);
path.pop_back(); // 回溯, 撤销处理节点
}
}
vector<vector<int>> combine(int n, int k) {
backtravel(n,k,1);
return result;
}
};
优化
已经选择的元素个数:path.size();
所需需要的元素个数为: k - path.size();
列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
完整code
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;
}
};
216.组合总和
写法一
按照上面组合问题,计算出所有组合,然后再最后的结束条件添加求和判断就解决了
class Solution {
public:
int num; // 表示 总和
vector<int> path; // 回溯数组
vector<vector<int>> result; // 结果
// 回溯数组 k 表示位数
void backtravel(int startIndex, int k) {
// 终止条件
if(path.size() == k) {
// 求和
int sum = 0;
for(int i = 0; i< path.size(); i++) {
sum += path[i];
}
if(sum == num ) {
result.push_back(path);
}
return;
}
for(int i = startIndex;i<= 9-(k-path.size())+1 ; i++) {
path.push_back(i);
backtravel(i+1,k);
path.pop_back(); // 出来
}
}
vector<vector<int>> combinationSum3(int k, int n) {
// 先找出组合数, 然后 相加如果等于 n 就添加到 result 二维数组中
num = n;
backtravel(1,k);
return result;
}
};
写法二
在递归的工程中,遍历实现求和。
剪枝操作
if (sum > num) { // 剪枝操作
return;
}
i <= 9-(k-path.size())+1
完整code
class Solution {
public:
vector<int> path; // 路径
vector<vector<int>> result; // 结果集
int num;
void backtravel(int sum, int k, int startIndex) {
if (sum > num) { // 剪枝操作
return;
}
if(path.size() == k) {
if(sum == num) {
result.push_back(path);
}
return;
}
// 逻辑
for(int i = startIndex;i<= 9-(k-path.size())+1;i++) {
path.push_back(i);
sum += i; // 处理
backtravel(sum, k, i+1);
sum -=i; // 回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
num = n;
backtravel(0,k,1);
return result;
}
};
17.电话号码的字母组合
图解
完整code
class Solution {
public:
// 结果
vector<string> result;
string s; // 字符串
// 映射
const string map[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
// 回溯
// index 表示 字符串 下标
void backtraval(string& digits,int index) {
// 终止条件
if(digits.size() == index ) {
result.push_back(s);
return;
}
// 一层逻辑
int num= digits[index] - '0'; // 表示 传入字符串对应的 整数
string str = map[num];
int size = map[num].size();
for(int i= 0; i< size; i++) {
s += str[i];
// s.push_back(str[i]);
backtraval(digits, index+1);
// s -= str[i];
s.pop_back();
}
}
vector<string> letterCombinations(string digits) {
//
if(digits.size() == 0) {
return result;
}
backtraval(digits,0);
return result;
}
};
39.组合总和
如果之前的题目都可以理解并写出来的话, 这个题目就是小改一下就可以了
图解
横向: 我们每次可以选的元素
纵向: 取完一个元素后, 下一个元素可以取的元素范围
步骤
三部曲
- 终止条件
总和为 target 添加到 result 直接 return
总和大于 target 直接return
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
- 单层逻辑
下面的 i 表示可选元素 的下标,因为题目说可以重复选择,所以这里调用backtravel不需要 i+1, 而是从头开始
也就是下标为 i 开始
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtravel(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
sum -= candidates[i]; // 回溯
path.pop_back(); // 回溯
}
完整code
class Solution {
public:
// 回溯递归
vector<int> path;
vector<vector<int>> result; // 结果
void backtravel(vector<int>& candidates,int target,int sum, int startIndex) {
// 终止条件
if(sum == target) {
result.push_back(path);
return;
}
// 大于
if(sum > target) {
return;
}
// 单层逻辑 下标
for(int i = startIndex; i< candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]); // 添加
backtravel(candidates,target,sum,i);
sum -= candidates[i];
path.pop_back(); // 回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtravel(candidates, target, 0, 0);
return result;
}
};
优化
主要是针对终止条件进行优化
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
完整code
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;
}
};