今天开始回溯算法相关的题目解答,虽然在此前多少接触过回溯算法,但系统性学习回溯算法是第一次,需要打好基础理解透彻。
77.组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
思路:
1.在系统接触回溯算法前,做这道题最初我想到的就是n层for循环,但这也引申出一个致命的问题:我又怎么知道有多少层for循环?且不论n层for循环写出来有多麻烦,在k未知的情况下我们根本不知道到底要多少层for循环。
2.而回想起之前二叉树的路径中,我们通过回溯找到二叉树的所有路径,和这道题进行类比不难发现,实际上这道题也可以转化成一个树形结构(而实际上所有的回溯题目都可以转化成类似树形的结构)。因此我们选择采用回溯法通过递归来解决问题,避免了不知道需要多少层for循环的问题。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex){
//路径数组中已经存储了k个数,压入结果数组中
if(path.size() == k){
result.push_back(path);
return;
}
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) {
backtracking(n, k, 1);
return result;
}
};
优化:
在递归遍历的过程中我们实际上遍历了许多无用的情况,例如假如n = 4, k = 4,那么当第一次压入的数为1时才有意义(即第一层for循环压入路径数组的第一个元素为1),如果第一次压入的数为2,后面最多也只能到2,3,4,不能满足k = 4的条件(注意唯一满足的1,2,3,4已经在第一层for循环的时候取到了)。
通过以上发现,我们进一步探寻规律。我们需要收集的一组元素个数为k,当前已经收集的元素个数为path.size(),仍需要收集的元素为k - path.size(),那么我们的遍历至少要从n - (k - path.size()) + 1处开始。至于为什么要加1,代入几个数值举例子后不难发现,我们要取的是一个左闭的区间,并且n是从1开始的。
由此,我们可以对代码进一步进行剪枝优化:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex){
//路径数组中已经存储了k个数,压入结果数组中
if(path.size() == k){
result.push_back(path);
return;
}
//进行剪枝优化,保证遍历从n - (k - path.size()) + 1开始才有意义
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) {
backtracking(n, k, 1);
return result;
}
};
启发:
1.首次系统学习回溯算法,步骤其实类似于递归,确定好函数的返回值;参数,满足条件返回的判断;单层循环的判断。几乎所有回溯法的问题都可以将问题转化为一个树形结构然后写出递归回溯的代码。
2.在进行回溯时难免会碰上无效的回溯递归,此时需要进一步进行剪枝优化操作。