1. 理论基础
-
什么是回溯算法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。 -
回溯法的效率
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,回溯法并不是什么高效的算法。 -
回溯法解决的问题
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等 -
如何理解回溯法
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。 -
回溯法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
2. 组合问题
思路: 使用回溯法,记录当前已记录过的添加进path的组合。使用for循环+递归,遍历剩余的可能添加的组合。传入一个索引,for循环从当前开始。
class Solution {
private:
vector<vector<int>> res; // 保存最后的结果
vector<int> path; // 保存单一的结果
public:
// 回溯法,startIndex记录开始的下标(从1开始),保证不重复
void backtracing(int n ,int k ,int startIndex) {
// 终止条件
if (path.size() == k) {
res.push_back(path);
return;
}
// 横向遍历,前面不变,当前位置每次变化,回溯
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracing(n, k, i + 1);
path.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracing(n, k, 1);
return res;
}
};
时间复杂度: O(n*2^n)
空间复杂度: O(n)
3. 组合优化
思路: 在for循环时,判断剩余数的个数与当前数组加起来是否大于等于k,若小于k,则可以直接跳出循环。
class Solution {
private:
vector<vector<int>> res; // 保存最后的结果
vector<int> path; // 保存单一的结果
public:
// 回溯法,startIndex记录开始的下标(从1开始),保证不重复
void backtracing(int n ,int k ,int startIndex) {
// 终止条件
if (path.size() == k) {
res.push_back(path);
return;
}
// 横向遍历,前面不变,当前位置每次变化,回溯
// 优化,i的值范围举例即可得到
for (int i = startIndex; i <= path.size()+n+1-k; i++) {
path.push_back(i);
backtracing(n, k, i + 1);
path.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracing(n, k, 1);
return res;
}
};
4.组合总和Ⅲ
思路: 和上题类似,只是多了剪枝操作,总体相同
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
// start为当前需添加的数,count 为当前path中的元素和
void backtracing(int n,int k, int start, int count) {
if (path.size() == k) {
if (count == n)
res.push_back(path);
return;
}
for (int i = start;i <= 10 + path.size() - k; i++) {
if (count + i > n) // 剪枝
break;
count += i;
path.push_back(i);
backtracing(n, k, i + 1, count);
count -= i;
path.pop_back();
}
return;
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
path.clear();
backtracing(n, k, 1, 0);
return res;
}
};
时间复杂度: O(n*2^n)
空间复杂度: O(n)
5. 电话号码的字母组合
思路: 利用回溯算法,先使用map将数字与字符数组对应,再回溯添加每个符合条件的元素。for循环中,遍历每个数字对应的字符数组的所有元素。
class Solution {
private:
// 定义一个字符数组用于映射电话号码,也可以用map映射
const string letterMap[10] = {
// 0,1,2,3,4,5,6,7,8,9
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
};
vector<string> res;
vector<char> path; // 记录单次结果,
// start为当前digits中的下标
void backtracing(const string digits,int start) {
if (start == digits.size()) {
string s = "";
for (char c : path)
s += c;
res.push_back(s); // 添加结果
return ;
}
int cur = digits[start] - '0'; // 记录当前的字符的int型
// 回溯,添加所有元素
for (int i = 0; i < letterMap[cur].size(); i++) {
// 也可使用string的push_back和pop_back进行回溯
path.push_back(letterMap[cur][i]);
backtracing(digits, start + 1);
path.pop_back();
}
return;
}
public:
vector<string> letterCombinations(string digits) {
res.clear();
path.clear();
if (digits == "") return res;
backtracing(digits, 0);
return res;
}
};
时间复杂度: O(3^m * 4^n) m 是对应三个字母的数字个数,n 是对应四个字母的数字个数
空间复杂度: O(3^m * 4^n)