回溯算法
-
回溯算法是一种试探性的搜索算法,它在解决某些组合问题、优化问题、路径问题等,非常有效。
回溯算法的核心思想是通过递归和深度优先搜索(DFS)来搜索问题的解空间
。 -
细说一下这些问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
回溯算法的主要步骤如下:
-
定义问题的解空间:首先要确定问题的解空间,它通常是一个树形结构,其中每个节点表示一个局部解或部分解。
-
深度优先搜索:从根节点开始,沿着深度方向逐个搜索节点。每次递归时,都会传递当前的解或部分解。
-
试探与回溯:当搜索到某个节点时,首先检查该节点是否满足问题的约束条件。如果满足,就继续向下搜索;如果不满足,就回溯到上一个节点,尝试其他可能的选择。这就是所谓的“试探与回溯”。
-
记录解或剪枝:在搜索过程中,当找到一个可行解或者达到搜索的最大深度时,需要记录这个解。在某些情况下,还可以通过剪枝(例如,剪去不可能产生更优解的子树)来加速搜索过程。
-
重复以上步骤,直到搜索完整个解空间或满足停止条件。
回溯算法的优缺点:
-
优点:
适用范围广泛:回溯算法适用于解决许多组合问题和优化问题,如八皇后问题、旅行商问题、图着色问题等。
容易理解和实现:回溯算法的逻辑较为简单,容易理解和实现。 -
缺点:
效率可能较低:回溯算法可能需要遍历整个解空间,当解空间很大时,算法的效率可能较低。
剪枝策略依赖于问题:在某些情况下,可以通过剪枝来加速搜索过程,但剪枝策略通常依赖于问题的特点,可能需要针对不同问题进行调整。
总之,回溯算法是一种通用、易理解的搜索算法,可以有效解决许多组合问题和优化问题。但需要注意,在解空间较大时,可能需要使用剪枝等手段提高搜索效率。
回溯算法解题模版
- 这里借用我最崇拜的Carl哥总结的回溯算法模板。
回溯算法解题三部曲:
第一步,找出回溯函数模板返回值以及参数
-
在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。
-
回溯算法中函数返回值一般为void。
-
再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
-
回溯函数伪代码如下:
void backtracking(参数)
第二步,确定回溯函数终止条件
-
既然是树形结构,就知道遍历树形结构一定要有终止条件,所以回溯也有要终止条件。
-
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
-
所以回溯函数终止条件伪代码如下:
if (终止条件) { 存放结果; return; }
第三步,回溯搜索的遍历过程
-
在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
-
如图:
-
注意图中,我特意举例集合大小和孩子的数量是相等的!
-
回溯函数遍历过程伪代码如下:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 }
-
for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
-
backtracking这里自己调用自己,实现递归。
-
大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
-
分析完过程,回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
这份模板很重要,后面做回溯法的题目都靠它了!