前言
今日在代码随想录中学习了关于回溯算法的相关知识,在这里记录一下所做的题目与一些回溯算法的相关知识
回溯算法
回溯算法是一种寻找路径的暴力求解算法,对于可以将问题抽象为一颗树然后去寻找到达叶子结点的问题可以使用回溯算法。如果不对回溯算法进行剪枝的操作的话,那么回溯算法就是一直暴力枚举。
一般来说,那种要求返回所有可能路径的问题可以使用回溯算法进行求解。
对于可以使用回溯算法求解的问题,一般都可以通过如下的3个步骤进行求解:
- 确定好回溯算法的返回值和传入参数
- 确定回溯函数的中止条件
- 仔细思考实现单层的搜索过程
在思考单层的搜索过程时一定需要记住,回溯算法之所以叫做回溯算法就是因为有一个回溯的过程,这是很重要的。具体的实现过程每个问题都会有所不同。
还可以再加入一步,回溯的剪枝,而这个剪枝的过程各有所不同,对于某些题目不一定可以很好的实现剪枝。
解题报告
1.力扣77
原题链接
题目概述
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
解题思路
对于这种返回各种组合的题目一般都可以使用这题的这种写法。先定义好一个用于返回的二维数组,然后再使用一个栈随时用于填充每一行的元素,同时有时也可以充当判断的条件使用。
首先是传入参数和返回值,返回值为空,传入参数这里加入了一个索引参数 start 在这种类型的题目中相当好用,具体的用途稍后解释。
接下来是中止的条件,对于此题来说,每次有满足条件的数字时都会将其填入栈中,而当栈内元素的个数达到了要求个数时,其实也就是说这一条路径已经结束了,那么就可以当做是中止条件,此时再将栈内的元素填入到结果数组中,记得return;
。
最后是较为复杂而又核心的单层搜索过程,这里简单的描述一下:首先这种问题一般都会使用for()
循环来实现,每一层的搜索过程实际上就是将当前的这一层进行遍历,然后对下一层进行递归调用,然后回溯。对于这题来说,直接将当前的元素入栈,然后递归调用本身,这里就体现了索引变量的好处了,按照题意,每一个元素都是不可以重复选取的,那么如果想要在下一层的递归中不使用到重复的元素的话,直接使得下一层从索引加一处开始循环即可。在递归调用后一定要记得回溯一下。
最后再来说说剪枝的问题,对于这题来说,剪枝体现在循环的范围压缩上,如果当前的可选数字加上栈中的元素个数已经小于了每一行需要的元素个数,那就不需要对这种情况进行讨论了,可以直接剪枝。
源码剖析
class Solution {
private:
vector<vector<int>> ans;
vector<int> stack;
void backtracking(int n,int k,int start){
if(stack.size()==k){
ans.push_back(stack);
return ;
}
for(int i = start;i<=n-(k-stack.size())+1;++i){
stack.push_back(i);
backtracking(n,k,i+1);
stack.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return ans;
}
};
2.力扣39
原题链接
题目概述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
解题思路
和上题相似但又有很多不同的地方。不同之处在于元素是可以重复选取的。一上来和上题一样先定义好两个数组。
首先是返回值和传入参数,这题的返回值一样是 void ,传入参数除去索引之外我还加入了一个用于判断是否中止的变量 sum。
中止条件:这题要求的是每个数组的和等于 target ,所以暂且不管怎么样实现 sum ,首先判断条件可以先写上,当 sum 等于 target 的时候就可以将当前的栈填入结果数组了。
单层搜素过程,一样的使用for()
循环,这题可以连带上剪枝一起讲,首先,这题如果要实现剪枝的话可以先对原数组进行排序,这样一旦当有元素加入时 sum 大于了 target 那么后面的元素都可以直接剪枝不去计算了。在可以填入的情况下,将当前的元素入栈,然后开始递归调用,递归的过程中,和要加上当前的入栈元素,但是索引不需要进行 +1 的操作了,因为当前元素也是可以重复选取的,直接从当前元素开始继续进行递归。然后记得回溯即可。
源码剖析
class Solution {
private:
vector<vector<int>> ans;
vector<int> stack;
void backtracking(vector<int>& candidates,int target,int sum,int start){
if(sum==target){
ans.push_back(stack);
return;
}
for(int i = start;i<candidates.size();++i){
if(sum+candidates[i]<=target){
stack.push_back(candidates[i]);
backtracking(candidates,target,sum+candidates[i],i);
stack.pop_back();
}else return;
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0);
return ans;
}
};
3.力扣131
原题链接
题目概述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
解题思路
其实和上面的题目很相似,这题就不详细展开了,定义了一个函数用于判断字符串是否为回文串。然后在循环过程中额外的定义一个空的临时字符串用于存入当前想要拿来切割的字符串,只有当这个字符串为回文串的时候才可以进行递归。
源码剖析
class Solution {
private:
vector<vector<string>> ans;
vector<string> stack;
bool judge(string s){
string stmp = s;
reverse(stmp.begin(),stmp.end());
return stmp == s;
}
void backtracking(string s,int start){
if(start==s.size()){
ans.push_back(stack);
return ;
}
string tmp = "";
for(int i = start;i<s.size();++i){
tmp+=s[i];
if(judge(tmp)){ //是回文串时才可以递归
stack.push_back(tmp);
backtracking(s,i+1);
stack.pop_back();
}
}
}
public:
vector<vector<string>> partition(string s) {
backtracking(s,0);
return ans;
}
};