1题目
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
2链接
题目链接:77. 组合 - 力扣(LeetCode)
视频链接:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili
3解题思路
第一反应找两个数的组合,两层for循环暴力求解,此题可行。
但是如果找50个数的组合,嵌套50层for循环根本不可能,时间复杂度极高,所以要用回溯算法
可以把这种问题都看成树状结构问题,如图所示:
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
图中每次搜索到了叶子节点,我们就找到了一个结果。
引入类似递归三部曲的回溯算法三部曲
1、确定函数返回值及参数
本题可以使用全局变量来记录结果,无需函数返回值。
注意到组合的特性:无顺序。所以选取的时候不能再反向选取,我们可以通过设定一个标志位startIndex来处理
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)
2、回溯的终止条件
如何终止?答:到达叶子结点(找到了目标组合)
其实也就是path的长度等于所需长度k
此时用result二维数组,把path保存起来,并终止本层递归。
if (path.size() == k) {result.push_back(path);return;
}
3、单层回溯的过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
如此我们才遍历完图中的这棵树。
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历path.push_back(i); // 处理节点backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始path.pop_back(); // 回溯,撤销处理的节点
}
可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。
注意下重点在于后面的撤销操作,这样才能起到回溯的效果
接上述内容
我们进行回溯求解时会发现,有很多树枝根本不可能产生结果,所以引入了新的优化方法:剪枝
举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在
第二层for循环,从元素3开始的遍历都没有意义了。如图:
说白了就是更改for循环的判别条件
思考一下整个过程:
1、我们使用一维数组path记录已选择的内容,已选择的个数为path.size()
2、我们所需的元素个数还剩:k-path.size()
3、列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
4、在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历。为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
所以优化后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
4代码
class Solution {
private:vector<vector<int>> result; // 存放符合条件结果的集合vector<int> path; // 用来存放符合条件结果void backtracking(int n, int k, int startIndex) {if (path.size() == k) {result.push_back(path);return;}// i为本次搜索的起始位置for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {//for (int i = startIndex; i <= n; i++) {path.push_back(i); // 处理节点backtracking(n, k, i + 1); // 递归path.pop_back(); // 回溯,撤销处理的节点}}
public:vector<vector<int>> combine(int n, int k) {result.clear(); // 可以不写path.clear(); // 可以不写backtracking(n, k, 1);return result;}
};