目录
一、回溯算法理论基础
回溯法:回溯法是一种将问题遍历的结构抽象为树形结构,在解空间树种采取深度优先策略,系统地搜索问题的所有解。
简单地来说,回溯法是一种在解空间树内暴力搜索的算法。可通过限界函数进行剪枝来提升算法效率。
回溯法在问题没有更优解法的时候,适用于求解答案是向量的问题,如搜索问题、排序问题、优化问题等。
回溯法的搜索空间是由问题的解构成的解空间树,常见的类型有:子集树、排列树、搜索树。
二、LeetCode 77. 组合
题目链接:LeetCode 77. 组合
文章讲解:代码随想录
视频讲解:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!
思路
一招鲜,吃遍天。组合问题是回溯法的经典题目,也可以作为所有回溯法(子集树)的一个代码模板来写。
要求在
1
∼
n
1\sim n
1∼n 中取出所有可能的组合,涉及到我们每一步选择哪个数加入组合的问题。在每一步(每一层)选择每个可能的数(遍历孩子结点)就可以抽象为一个子集树的遍历过程,于是可以使用回溯算法进行遍历求解。
在回溯过程中,重要的部分在于如何表述每一层中选择不同孩子的逻辑,以及遍历子集树时进入下一层遍历的传参问题。如上图,在本问题中,我们将当前位置选择哪一个数作为不同的孩子结点,那么我们可以写一个for
循环来完成不同孩子结点之间的遍历,用递归实现进入下一层:
for(int i = pre + 1; i <= n; i++){ //递归构建子集树
set[t] = i;
backtrack(t+1, i, n, k, set);
}
set
使用全局变量的话可以这样写:
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
这样更为规范,可读性也更好一些。
终止条件设置为set
中元素数量等于k
即可。
在上述代码的基础上,可以做一定的剪枝提高时间效率:我们可以发现,在前一位数选择了n-(k-set.size())+1
时,即前面数选择以后,后面的数不够分,达不到k
个的数目,那么这些分支实际上都是无效遍历;也就是说,在组合中,每一位上选择的数是有一个右边界的,我们通过找规律不难发现,这个右边界就是n-(k-set.size())+1
。
那么遍历代码可以优化为:
for (int i = startIndex; i <= n - (k - set.size()) + 1; i++) // i为本次搜索的起始位置
C++代码
class Solution {
public:
vector<vector<int>> subsets;
void backtrack(int t, int pre, int n, int k, vector<int> set){
if(t == k){
subsets.push_back(set);
return;
}
for(int i = pre + 1; i <= n-(k-t)+1; i++){ //递归构建子集树
set[t] = i;
backtrack(t+1, i, n, k, set);
}
}
vector<vector<int>> combine(int n, int k) {
vector<int> set(k);
backtrack(0, 0, n, k, set);
return subsets;
}
};
二、LeetCode 216.组合总和III
题目链接:LeetCode 216.组合总和III
思路
本题与上题较为相似,不同之处只在于使用数字变为一到九,并增加一个总和条件。我们只调节可选取的数字,并增加一个总和相等的终止条件即可。
C++代码
class Solution {
private:
vector<vector<int>> sumsets;
vector<int> set;
void backtrack(int pre, int sum, int k, int n){
if(sum > n) return; //剪枝操作:总和大于要求值返回
if(set.size() == k){ //终止条件
if(sum == n){
sumsets.push_back(set);
}
return;
}
for(int i = pre + 1; i <= 9-(k-set.size())+1; i++){ //数字选取1--9
set.push_back(i);
sum += i;
backtrack(i, sum, k, n);
sum -= i;
set.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtrack(0, 0, k, n);
return sumsets;
}
};
二、LeetCode 17.电话号码的字母组合
文章讲解:代码随想录
视频讲解:还得用回溯算法!| LeetCode:17.电话号码的字母组合
思路
本题也是很简单的组合问题,主要解决的点在于电话号码上字母的映射,可以选择建立一个全局变量数组存储每个数字对应的字母,笔者采用 A S C I I ASCII ASCII 码转换的方法进行映射。其余操作与基本的回溯法相同。
C++代码
class Solution {
private:
vector<string> combinations;
string letters;
void backtrack(string digits, int index){
if(index == digits.size()){
combinations.push_back(letters);
return;
}
int del = 3;
int be;
if(digits[index] > '1' && digits[index] < '7'){
be = 43 + int(digits[index]-'0') * 2;
}
else if(digits[index] == '7'){
be = 57;
del = 4;
}
else if(digits[index] == '8'){
be = 60;
}
else if(digits[index] == '9'){
be = 62;
del = 4;
}
for(int i = be; i < be+del; i++){
letters.push_back(digits[index] + i);
backtrack(digits, index+1);
letters.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if(digits.empty()) return combinations;
backtrack(digits, 0);
return combinations;
}
};
总结
回溯法章节开始,逐渐开始正式复习算法设计课的知识。
文章图片来源:代码随想录 (https://programmercarl.com/)