回溯算法详解
Last renew: September 17, 2022 pm
回溯问题概述
注:本文仅为个人学习笔记,无任何版权。
注2: dfs与回溯之间并不是并列的关系,dfs是一种深度优先的遍历方法,而回溯是一种解决问题的手段。dfs可以有回溯,回溯也可以用dfs。
什么是回溯算法?
回溯法也可以叫做回溯搜索法,是一种搜索的方式。回溯是递归的副产品,有递归就会有回溯。
回溯法的效率
回溯法的性能如何呢?实际上不是什么高效的算法。因为本质是穷举。如果想要回溯法高效,可以增加一些剪枝的操作。但效率仍然一般,因为其本质就是穷举。
回溯法适合解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等
注:组合和排列的区别是组合不强调元素顺序,排列强调元素顺序。
如何理解回溯法
回溯法解决的问题都可以抽象为树形结构!
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度则构成了树的深度。
使用递归就要有中止条件,所以树的高度是有限的。
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考三个问题。
- 路径:也就是已经作出的选择。
- 选择列表:也就是你当前可以做的选择。
- 结束条件:也就是到达决策树底层,无法再做选择的条件。
1 |
|
核心就是for循环里的递归,在递归调用之前做选择,在递归调用之后撤销选择。
可以看出for循环就是横向的遍历,backtracking就是纵向遍历,这样就把这棵树全遍历完了。
这个框架的奥秘在哪里呢?我们来看两道例题:
1. 全排列问题
高中的排列组合数学题。对于n个不同的数,全排列共有n!个。
穷举全排列的方法?[1, 2, 3] [1, 3, 2] [2, 1, 3] [2, 3, 1]…..
可以按照这个逻辑画出一颗决策树。
只要从root遍历这棵树,记录路径上的数字,其实就是所有的全排列。
假如我们进行穷举走到了[2, x, y]的分叉,这时候2的含义就是[路径],用于记录你已经做过的选择;
而[1]或[3]则是[选择列表]。表示你当前可以做出的选择;
[结束条件]就是遍历到树的底层,在这里就是选择列表为空的时候。
这时候可以看出,我们所定义的backtrack函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其[路径]就是一个全排列。
那么,如何遍历这棵树?
简单的二叉树遍历罢了,前序遍历 or 后序遍历。
1 |
|
稍微回忆一下,前序遍历的代码会在进入某一个节点之前的时间执行,而后序遍历代码会在离开某个节点之后的那个时间点执行。
而就在刚才我们所述,[路径]和[选择]是每个节点的属性,函数在树上游走要正确维护节点的属性。所以我们应当在前序和后序两个位置选择不同的操作。
前序为做出选择,而后序为撤销选择。
1 |
|
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确的得到每个节点的选择列表与路径。
1 |
|
自此,我们就通过全排列问题了解了简单的回溯算法。当然,这个算法解决全排列并不高效,contains
方法需要O(N)的时间复杂度。有更好的方法可以follow up
但不论怎样优化,只要符合回溯框架,时间复杂度都不可能低于O(N!),因为穷举整棵树是无法避免的。一般回溯算法时间复杂度都很高。