回溯
回溯是 DFS 中的一种技巧。回溯法采用 试错 的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
通俗上讲,回溯是一种走不通就回头的算法。
回溯的本质是穷举所有可能,尽管有时候可以通过剪枝去除一些根本不可能是答案的分支, 但是从本质上讲,仍然是一种暴力枚举算法。
回溯法可以抽象为树形结构,并且是是一颗高度有限的树(N 叉树)。回溯法解决的都是在集合中查找子集,集合的大小就是树的叉树,递归的深度,构成树的高度。
以求数组 [1,2,3] 的子集为例:

for 循环用来枚举分割点,其实区间 dp 分割区间就是类似的做法
以上图来说, 我们会在每一个节点进行加入到结果集这一次操作。

对于上面的灰色节点, 加入结果集就是 [1]。

这个加入结果集就是 [1,2]。

这个加入结果集就是 [2,3],以此类推。一共有六个子集,分别是 [1], [1,2], [1,2,3], [2], [2,3] 和 [3]。
而对于全排列问题则会在叶子节点加入到结果集,不过这都是细节问题。掌握了思想之后, 大家再去学习细节就会事半功倍。
下面我们来看下具体代码怎么写。
算法流程
构造空间树。
进行遍历。
如遇到边界条件,即不再向下搜索,转而搜索另一条链。
达到目标条件,输出结果。
算法模板
伪代码:
剪枝
回溯题目的另外一个考点是剪枝, 通过恰当地剪枝,可以有效减少时间,比如我通过剪枝操作将石子游戏 V的时间从 900 多 ms 优化到了 500 多 ms。
剪枝在每道题的技巧都是不一样的, 不过一个简单的原则就是避免根本不可能是答案的递归。
举个例子: 842. 将数组拆分成斐波那契序列
题目描述:
还是直接套回溯模板即可解决。但是如果不进行合适地剪枝,很容易超时,这里我进行了四个剪枝操作,具体看代码。
剪枝过程用图表示就是这样的:

剪枝算法回溯的一大考点,大家一定要掌握。
笛卡尔积
一些回溯的题目,我们仍然也可以采用笛卡尔积的方式,将结果保存在返回值而不是路径中,这样就避免了回溯状态,并且由于结果在返回值中,因此可以使用记忆化递归, 进而优化为动态规划形式。
参考题目:
这类问题不同于子集和全排列,其组合是有规律的,我们可以使用笛卡尔积公式,将两个或更多子集联合起来。
经典题目
总结
回溯的本质就是暴力枚举所有可能。要注意的是,由于回溯通常结果集都记录在回溯树的路径上,因此如果不进行撤销操作, 则可能在回溯后状态不正确导致结果有差异, 因此需要在递归到底部往上冒泡的时候进行撤销状态。
如果你每次递归的过程都拷贝了一份数据,那么就不需要撤销状态,相对地空间复杂度会有所增加。
最后更新于
这有帮助吗?