代码随想录算法训练营第二十四天|回溯算法、Leetcode77 组合
● 回溯算法
● 回溯算法介绍
回溯算法实际上是一个类似枚举的搜索过程,目的是再搜索尝试的过程中国寻找到问题的解,当发现条件不满足时就会进行回溯,尝试其他路径。最简单的一个例子就是迷宫,求解条件为找到迷宫出口,我们从入口出发,会遇到很多岔路,也就是回溯算法的不同路径,当遇到“死路”的时候,即求解条件不满足,需要回溯走其他可能通向终点的路径,直到获得从入口到出口的路径集;
同时,回溯与递归会同时出现,往往在递归的下一步就是进行回溯。
因为回溯是一种搜索尝试的算法,也就是说回溯的本质是穷举,列举出所有可能,然后在可能集中找出结果集,在该过程虽然可以进行一些剪枝操作,但回溯的穷举本质没有改变,因此回溯算法并不高效;但采用回溯算法虽然效率比较低,但对于某些问题而然使用回溯算法避无可避,因此当遇到特定问题,只能使用回溯算法进行解决。
● 回溯算法解决的问题
我们将回溯算法解决的问题进行以下分类:
(1)组合问题:N个数里面按照一定规则找出k个数的集合;
(2)切割问题:一个字符串按一定规则有几种切割方式;
(3)子集问题:一个N个数的集合中有多少符合条件的子集;
(4)排列问题:N个数按一定规则排列,有几种排列方式;
(5)棋盘问题:N皇后、数组等问题。
● 理解回溯算法
在二叉树遍历、深度优先搜索中都与递归回溯组合在一起,对于回溯算法解决的问题也都可以抽象为树形结构进行解决。因为回溯算法解决的都是在集合中递归查找自己,集合的大小构成了树的宽度,递归的深度构成了树的深度,除了子集问题,其他几类问题的解都是在叶子结点处返回结果。
● 回溯算法模板
(1)回溯函数参数和返回值
回溯算法中函数返回值一般为void
,传入参数则需要依据具体实现逻辑才能确定。
void backtracking(<参数>)
(2)回溯函数终止条件
和二叉树递归一样,回溯函数拥有自己的终止条件,一般为搜索到叶子结点存放答案后结束递归。
if(<终止条件>)
{
<存放结果>;
return;
}
(3)回溯算法的遍历过程
回溯算法一般实在集合中递归搜索,集合大小构成树的宽度,递归的深度构成树的深度,因此回溯算法的遍历过程一般为:
for(<选择:本层集合中元素>)
{
<处理节点>;
backtracking(<路径>,<选择列表>);
<回溯,撤销处理结果>;
}
对于回溯算法的遍历过程,for循环因为是遍历一层集合中的元素,因此是横向遍历,而backtracking是回撤某一条路径上的操作,是纵向遍历,两者结合能够将所有可能结果遍历完。
● Leetcode77 组合
题目链接:Leetcode77 组合
视频讲解:代码随想录|组合
题目描述:给定两个整数 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]]
提示:
· 1 <= n <= 20
· 1 <= k <= n
● 解题思路
在回溯算法理论中提到,每一个回溯算法的问题都可以抽象为树形结构解决。
以示例一为例,回溯深度为2,单层元素为4:
按照回溯三部曲:
(1)确定回溯函数参数和返回值:
代码定义了全局变量result
保存结果集,path
记录当前路径,因此返回值为void
,传入参数为整数n和k
,同时下一次回溯函数中的元素为去除前一个元素之后的剩余全部,因此我们需要从i + 1
个元素开始寻找结果;
(2)确定回溯函数终止条件:
对于抽象的树状结构,终止条件为到叶子结点时将该条路径的元素存入结果集中,对应到题目也就是当path中的元素大小与k相等时,存入结果;
(3)确定回溯算法的遍历过程:
我们用startIndex记录每次回溯算法开始的位置,每向后遍历一个元素,就向path中加入,在回溯结束之后需要将path的元素弹出,进行回溯操作,也就是从其他回退一步继续走其他路径。
同时,对于本题可以进行剪枝操作;所谓剪枝操作,就是将不满足条件的路径wipe off掉,如示例一中因为当startIndex遍历到4时,不满足path.size() == 2,因此这条路径就被剪枝:
剪枝操作被体现在for循环中,我们将i的遍历范围进行修改即可剪枝;
不难发现,当path
内插入元素,则还能再向path中插入k - path.size()
个元素,对于n个数而言,当i遍历到n - (k - path.size()) + 1
时,之后的元素就无法满足返回条件,即被剪枝。
● 代码实现
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) {
backtracking(n, k, 1);
return result;
}
};
剪枝代码:
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 - (k - path.size()) + 1); i++)
{
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};