回溯-day65
回溯
什莫事回溯
回溯法也可以叫做回溯搜索法,它是一种搜索的方式
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质
回溯法并不高效为什么还要用它呢?
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
试错与回退
回溯法从解空间树的根节点出发,按深度优先策略逐层探索。若当前路径不满足约束条件或无法达到目标,则回退到上一状态(撤销最后一步选择),继续尝试其他分支
剪枝优化
在搜索过程中,通过预判当前路径的有效性,提前终止无效分支的探索(例如检测到冲突时),避免不必要的计算
递归算法!
- 确定递归函数的参数和返回值:
- 确定终止条件:
- 确定单层递归的逻辑:
回溯算法!
/*
vector<vector<int>> result; // 存储所有解
vector<int> path; // 当前路径
*/
void backtracking(参数) {
if (终止条件) {
存放结果; //result.push_back(path);
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点; // if (剪枝条件) continue; // 跳过无效分支
// path.push_back(选择); // 记录选择
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果 // path.pop_back();
}
}
组合
77. 组合 - 力扣(LeetCode)
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
直接的算法是for循环 ,遍历每个位数可以选择的数😒
当k=2时 ,用两个for循环
for(int i=0;i<=n;i++){
for(int y=i+1;y<=n;y++) //因为是组合不是排列,不用在选出现过的数
cout<<i<< " "<<y<<endl;
}
如果n为100,k为50呢,如何写那么多for循环呢,while …?
虽然想暴力搜索,但是用for循环嵌套好像写不出来🤔
要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,
那么回溯法就用递归来解决嵌套层数的问题
递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了
由于回溯法解决的问题都可以抽象为树形结构(N叉树),用树形结构来理解回溯就容易多了🙂
图中可以发现n相当于树的宽度,k相当于树的深度
图中每次搜索到了叶子节点,我们就找到了一个结果。
相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。
剪枝优化
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了,如图4不用了
优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()-1); i++) // i为本次搜索的起始位置
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;
}
};
组合总和III
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
216. 组合总和 III - 力扣(LeetCode)
😍
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
int sum=0;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k&&sum==n) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= 9 ; i++) {
path.push_back(i);
sum+=i;
backtracking(n, k, i + 1);
sum-=i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking( n, k, 1);
return result;
}
};
剪枝优化 😕
其实这里sum这个参数也可以省略,每次targetSum (n)减去选取的元素数值,然后判断是否targetSum为0了
for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了
for (int i = startIndex; i <= n - (k - path.size()-1); i++) // i为本次搜索的起始位置
已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了
if (sum > n) { // 剪枝操作
return;
}
确定终止条件
k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。
所以如果path.size() 和 k相等了,就终止
优化结果🆗
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (sum > targetSum) { // 剪枝操作
return;
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝
sum += i;
path.push_back(i);
backtracking(targetSum, k, sum, i + 1);
sum -= i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 0, 1);
return result;
}
};
电话号码的字母组合
17. 电话号码的字母组合 - 力扣(LeetCode)
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
确定回溯函数参数
- 字符串s来收集叶子节点的结果
- k是记录遍历第几个数字
确定终止条件
- 遍历第几个数字等于 输入的数字个数
- 或已存入数字个数等于 输入的数字个数
确定单层遍历逻辑
- for循环
本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,
而77. 组合 (opens new window)和216.组合总和III (opens new window)都是求同一个集合中的组合!
class Solution {
public:
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 path;
int sum=0;
void backtracking(string digits, int k) {
if (path.size() ==digits.size()) {
result.push_back(path);
return;
}
int digit = digits[k] - '0';
string letters = letterMap[digit];
for(auto t:letterMap[digit]){
path.push_back(t);
backtracking(digits, k+1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) {
return result;
}
backtracking(digits, 0);
return result;
}
};