回溯法理论基础
回溯的本质是一种穷举,之所以要用琼剧来解决问题,是因为有些问题如果不用穷举的话升值不好写出来,因此用递归回溯的方法来解决,当然,一些剪枝操作还是可以做的。
那么,都说回溯这种方法和递归很像,很像在哪呢?通过回溯三部曲,我们就能有所体会。
回溯的第一步是要确定函数的返回值和函数参数,第二步是要确定终止条件,接着保存结果,return,第三步是确定单层递归逻辑,可以发现,回溯的解题步骤和递归是一脉相承的。
当然,要想深入了解回溯,还是要结合具体的问题来进行理解。
力扣77、组合
细节:首先区分下组合和排列,组合是无序的,排列是有序的。
思路:对于本题而言,如果用暴力解的话,那么就是用for循环去嵌套遍历了,如果k很大的话那这个循环是没法写的,这个时候我们就可以考虑回溯了,我们需要两个全局变量,一个变量保存结果集,一个变量用二维数组保存各个结果集。
首先明确函数的返回值,一般回溯的返回值都是void,接着确定函数的参数,我们肯定要用到题目给的n和k,还需要什么呢?我们现在还不知道,在做回溯的时候,我们往往无法一次性写完所有参数,在书写逻辑的时候,缺什么补什么就可以了,现在让我们进行第二部,终止条件的确定,本题终止的条件就是当前结果集中的元素已经达到k个时,我就可以结束递归,保存结果了。第三步单层递归的逻辑,我们用for循环去横向便利,也就是说集合的元素个数决定了树的宽度,用递归回溯去纵向遍历,也就是用k的大小去决定树的深度,这样我们就可以遍历完所有结果了。
代码:
class Solution {
public:
vector<vector<int>>result;//保存结果集的集合
vector<int>path;//保存结果集
void backtracking(int n,int k,int startIndex){
if(path.size()==k){
result.push_back(path);
return;
}
//单层递归的逻辑
//先要用for循环来横向遍历,决定树的宽度,回溯来纵向遍历,决定树的深度。
for(int i=startIndex;i<=n;++i){
path.push_back(i);
backtracking(n, k, i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};
优化:不难看出,这份代码中有一些情况显然没有继续下去的价值,比如说n=4,k=3的时候,如果从3开始遍历,根本就找不到答案,3之后就更不用说了,这个时候我们就可以进行一些剪枝的操作,对于本题而言:
结果集中的元素个数为k;
当前还需要的元素个数为k-path.size();
至多从下标为n-(k-path.size())+1开始遍历
例如,n=4,k=3,假设当前结果集中元素数量为0,那么至多从2开始遍历,没毛病。
因此,对于下标大于它的元素,就没有进入for循环的必要了。
这就是剪枝操作,也可以提高效率。
代码:
class Solution {
public:
vector<vector<int>>result;//保存结果集的集合
vector<int>path;//保存结果集
void backtracking(int n,int k,int startIndex){
if(path.size()==k){
result.push_back(path);
return;
}
//单层递归的逻辑
//先要用for循环来横向遍历,决定树的宽度,回溯来纵向遍历,决定树的深度。
for(int i=startIndex;i<=n-(k-path.size())+1;++i){//优化的地方
path.push_back(i);
backtracking(n, k, i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};