回溯算法理论基础
1.什么是回溯法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
2.回溯法解决的问题
(1)组合问题:N个数里面按一定规则找出k个数的集合。
例如:在{1,2,3,4}中找出大小为2的组合,12、13、14、23、24、34。
(2)切割问题:一个字符串按一定规则有几种切割方式。
(3)子集问题:一个N个数的集合里有多少符合条件的子集。
(4)排列问题:N个数按一定规则全排列,有几种排列方式。
组合没有顺序,排列有顺序,例如:{1,2}只有一种组合12,而有两种排列12和21。
(5)棋盘问题:N皇后,解数独等等。
使用for循环层层嵌套解决不了的或者写不出这样的代码,使用回溯算法。
3.如何理解回溯算法
回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
4.回溯算法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合 leetcode 77
解法一:无剪枝操作
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(int n,int k,int index){
if(path.size()==k){
res.push_back(path);
return;
}
for(int i=index;i<=n;i++){
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracking(n,k,1);
return res;
}
};
把整个回溯过程抽象为一棵树形结构,for循环遍历树的宽度,递归遍历树的深度。
解法二:解法一基础上做减枝操作
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(int n,int k,int index){
if(path.size()==k){
res.push_back(path);
return;
}
for(int i=index;i<=n-(k-path.size())+1;i++){
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
return;
}
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracking(n,k,1);
return res;
}
};
总结
在1到n中取k个数进行组合,如果我们已经取了m个数,还需要取k-m个数,所以我们最多要从n-(k-m)+1处开始取,再往后就取不到k个数了,就是我们要减枝处理的部分。
组合总和Ⅲ leetcode 216
解法一:无减枝操作
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum=0;
void backtracking(int k,int n,int index){
if(sum==n&&path.size()==k) {
res.push_back(path);
return;
}
for(int i=index;i<=9;i++){
path.push_back(i);
sum+=i;
backtracking(k,n,i+1);
path.pop_back();
sum-=i;
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
path.clear();
backtracking(k,n,1);
return res;
}
};
当path收集到k个数且总和达到n时结束递归。for循环遍历宽度,递归函数遍历深度,调用递归函数之后回溯。
解法二:解法一基础上减枝操作
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int sum=0;
void backtracking(int k,int n,int index){
if(sum>n) return;
if(sum==n&&path.size()==k) {
res.push_back(path);
return;
}
for(int i=index;i<=9-(k-path.size())+1;i++){
sum+=i;
path.push_back(i);
backtracking(k,n,i+1);
sum-=i;
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
path.clear();
backtracking(k,n,1);
return res;
}
};
减枝累加和大于n的情况,减枝取不到k个元素的情况。
电话号码的字母组合 leetcode 17
class Solution {
private:
const string latterMap[10]={
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
};
public:
vector<string> res;
string s;
void backtraking(string& digits,int index){
if(index==digits.size()){
res.push_back(s);
return;
}
int digit=digits[index]-'0';
string letters=latterMap[digit];
for(int i=0;i<letters.size();i++){
s.push_back(letters[i]);
backtraking(digits,index+1);
s.pop_back();
}
}
vector<string> letterCombinations(string digits) {
s.clear();
res.clear();
if(digits.size()==0) return res;
backtraking(digits,0);
return res;
}
};
总结
1.index为digits的索引,当index=digits.size()-1时还需要往s里面添加一个字母,所以循环结束条件应该是index==digits.size()。
2.在C++中,字符的ASCII码值可以直接进行算术运算。字符 '2' 的ASCII码值是 50,字符 '0' 的ASCII码值是 48,所以字符'2'减去字符'0'就是整数2。