原文链接:组合问题
————————————————
一、参考资料
二、回溯法相关介绍
递归和回溯是一家,回溯和递归都是相伴相生的。
回溯是递归的副产品,只要有递归就会有回溯。
1. 问题引入
一些问题只能通过暴力搜索,没有其他更高效的解法,因此选择回溯算法。
2. 回溯法的适用场景
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合;
- 切割问题:一个字符串按一定规则有几种切割方式;
- 子集问题:一个N个数的集合里有多少符合条件的子集;
- 排列问题:N个数按一定规则全排列,有几种排列方式;
- 棋盘问题:N皇后,解数独等等。
3. 回溯法的本质
回溯法的本质是穷举,穷举所有可能,然后选出我们想要的答案。因此,回溯法的性能并不高。为了提高回溯法的效率,可以加一些剪枝的操作。
4. 理解回溯法
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小构成树的宽度,递归的深度构成的树的深度。
回溯法的基本做法是深度优先搜索,是一种组织得井井有条的、能避免不必要重复搜索的穷举式搜索算法。
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
5. 回溯算法流程
5.1 确定回溯函数的参数和返回值
- 函数名,在回溯算法中,函数名通常为backtracking();
- 返回值,回溯算法中函数返回值一般为void。
- 参数,回溯算法需要的参数不像二叉树递归那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
回溯函数伪代码如下:
void backtracking(参数)
5.2 确定回溯法的终止条件
一般来说,搜索到叶子节点,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
回溯函数终止条件伪代码如下:
if (终止条件) {
存放结果;
return;
}
5.3 回溯搜索的遍历过程
通常,回溯法是在集合中递归搜索,集合的大小构成树的宽度,递归的深度构成树的深度。
回溯函数遍历过程伪代码如下:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
从图中看出 ,for循环用来横向遍历,backtracking(递归)用来纵向遍历,这样就可以把整棵树遍历完。一般来说,搜索叶子节点就是找其中一个结果。
分析完过程,回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
三、示例分析
1. 示例源码
源码请参考:代码随想录-第77题. 组合
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;
}
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;
}
};