1.带你学透回溯算法(理论篇)
回溯法解决的问题可以抽象为N叉树
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,每次搜索到了叶子节点,我们就找到了一个结果。
n相当于树的宽度,k相当于树的深度。
class Solution {
public:
vector<vector<int>> result; //存放符合条件的结果集合
vector<int> path; //存放已经被选出的数字
void backtracking(int n, int k, int start) { //通过start在集合中选取元素、调整可选择的范围
if (path.size() == k) { //path数组大小等于k,说明找到了一个子集大小为k的组合
result.push_back(path);
return;
}
for (int i = start; i <= n; i++) { //控制树的横向便利
path.push_back(i); //将遍历过的节点加入
backtracking(n, k, k + 1); //递归:控制树的纵向遍历,下一层从i+1开始
path.pop_back(); //回溯,撤销处理的节点
}
}
vector<vector<int>> combinde(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
当n = 4,k = 4时,第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了,因此可以对其进行剪枝优化
那么就可以在递归中每一层的for循环所选择的起始位置进行剪枝,如果for循环选择的起始位置之后的元素个数不满足需要的元素个数,那么就没有必要搜索了。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int start) {
if (path.size() == k) {
result.push_back(path);
return;
}
//path.size()为已经选择的元素个数,那么k - path.size()就是还需要的元素个数,那么最多需要从n - (k - path.size()) + 1的地方遍历,+1是要包括起始位置
for (int i = start; 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;
}
};
本题k就相当于树的深度,9相当于树的宽度。
解题的思路和77题差不多,只不过多了一个和的限制
class Solution {
public:
vector<vector<int>> result; //存放符合条件的结果集合
vector<int> path; //存放根节点到叶子节点的路径
void backtracking(int target, int k, int sum, int start) { //target为目标和,sum为已经收集的元素的总和(path里元素的总和),start为下一层for循环搜索的起始位置
if (path.size() == k) { //path数组大小等于k,说明找到了子集大小为k且和等于目标值的组合
if (sum == target) result.push_back(path);
return; //如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = start; i <= 9; i++) { //控制树的横向便利
sum += i;
path.push_back(i); //将遍历过的节点加入
backtracking(target, k, sum, i + 1); //递归:控制树的纵向遍历,下一层从i+1开始
sum -= i;
path.pop_back(); //回溯,撤销处理的节点
}
}
vector<vector<int>> combinde(int n, int k) {
backtracking(n, k, 0, 1);
return result;
}
};
剪枝操作同样和77题差不多,当已选元素总和如果已经大于n了,那么往后遍历就没有意义了,直接剪掉
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int target, int k, int sum, int start) {
if (sum > target) return; //剪枝操作
if (path.size() == k) {
if (sum == target) result.push_back(path);
return;
}
for (int i = start; i <= 9; i++) {
sum += i;
path.push_back(i);
backtracking(target, k, sum, i + 1);
sum -= i;
path.pop_back();
}
}
vector<vector<int>> combinde(int n, int k) {
backtracking(n, k, 0, 1);
return result;
}
};
可以通过哈希表或者二维数组存储每个数字对应的字母,再通过回溯算法寻找所有可能的解
class Solution {
public:
const string letterMap[10] = { //或unordered_map phoneMap{{'2', "abc"},{'2', "abc"}...};
"", // 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) { //index记录遍历到digits的第几个数字
if (digits.size() == index) {
result.push_back(s);
return;
}
int digit = digits[index] - '0'; //将index指向的数字转为int类型
string letters = letterMap[digit]; //取出数组对应的字符,将其中的一个字母插入到已有的字母排列后面
for (int i = 0; i < letters.size(); i++) { //然后继续处理电话号码的后一位数字,直到处理完所有数字,即得到一个完整的字母排列
s.push_back(letters[i]);
backtracking(digits, index + 1);
s.pop_back();
//上面三句可以简化为 backtracking(digits,index+1,s+letters[i]); 传入参数也要修改
}
}
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) return result;
backtracking(digits, 0);
return result;
}
};