回溯算法是一种很重要的算法,有着通用算法的美称,不管是leetcode也好还是考研、笔试也罢都会有大量回溯算法的题目出现。该文章首先会解决什么叫做回溯算法,然后以leetcode题目《46. 全排列》、 leetcode题目《131. 分割回文串》作为例题,来讲解如何思考回溯算法、怎么样进行回溯,最后总结回溯模板。题目讲解用伪代码,是为了让JAVA、Python、C++等语言的靓仔、靓女搞明白,文末有具体实现的链接。
文章目录
致敬一下大佬的文章:leetcode用户liweiwei1419对题目《46. 全排列》的题解《 从全排列问题开始理解“回溯搜索”算法(深度优先遍历 + 状态重置 + 剪枝》,网址:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/,给了许多启发,也是通过这篇文章,我弄懂了这个回溯鬼东西是怎么玩的。
1. 用leetcode题目《46.全排列》做例题,方便讲解
所有的算法的提出都是为了解决比较实际的问题,当然用工程方面的东西太复杂了,这里用leetcode的题目。
leetcode题目《46. 全排列》,网址:https://leetcode-cn.com/problems/permutations/
2. 回溯算法——穷举法的2.0版本
回溯算法又叫回溯搜索算法,核心思路是将一个问题中所有可能出现的情况(穷举法)转化为解集树,然后逐一剔除剪枝找到符合条件的解又或者是解集(穷举的优化)。 “回溯”指的是“状态重置”,也就是一条路走不通回到原点在走一次。这种“剔除”技巧叫做剪枝技巧。常见的应用是求解子情况。
回溯法思路的简单描述是:把问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。
那么为了方便理解和构造回溯算法的模型,在这里我将全排列问题中所有可能出现的情况以树状图的形式列举出来,(一条路径一个解)
回溯,且剪去不符合条件的解之后。
2.1 等一下,这个东西好像在哪里见过
其实深度优先搜索就用到了回溯算法的思想,都是在探索过程中搜索可能符合条件的解,一种大的情况探索完了,在换一种情况,此路不通换一条路。不太熟悉深度优先搜索的,可以看一下我自己的博客《深度优先搜索DFS(动画解算法,内附C++/C、JAVA、Python的实现)》
不同点在于深度优先搜索是在探索一个比较明显的图(当然也有用在树的遍历),而回溯算法探索的是一个比较隐晦的解集树。
深度优先搜索的往回走的过程:
2.2 为什么要回溯,头铁撞南墙不香吗?
为什么要回溯?回溯是为了不用走那么多路,从而减少时间复杂度。
还走1 1 * 这条路,怕是直接把我头按在电饭煲里面去哟。又不是我方打野在野区刷微信步数。
2.3 同样是探索可能出现的所有情况,为什么搜索的时候不用广度优先搜索
常见的探索方法有广度(BFS)和深度(DFS),为什么回溯算法只用深度优先搜索呢?
最主要是深度没有这么麻烦,不用设置队列等乱七八糟的。
- 只有遍历所有可能出现的情况,才能得到所有符合条件的解
- 在深度优先遍历的时候,不同子情况之间(比如说 1 2 3 切换到 1 3 2)切换很容易,每两个子情况之间的差别只有1处,因此往后走,直接对整个路径的(比如说:1 1 * 这条路在1的时候就可以放弃了)放弃比较容易,全局用一份状态变量就可以完成搜索。
- 广度优先搜索要用到队列还要编写结点类别,比较麻烦。使用深度优先搜索,直接用了系统的栈,系统栈帮助我们保存了每一个结点的状态信息。于是我们不用编写结点类,不必手动编写栈完成深度优先遍历。简单粗暴有效
- 如果使用广度优先遍历,从浅层转到深层,状态的变化就很大,此时我们不得不在每一个状态都新建变量去保存它,从性能来说是不划算的;
参考了:leetcode用户liweiwei1419对题目《46. 全排列》的题解《从全排列问题开始理解“回溯搜索”算法(深度优先遍历 + 状态重置 + 剪枝)》,网址:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
我自己的博客:《广度/宽度优先搜索 BFS (动画解算法 附C++\C、JAVA、Python的代码实现》
3. 以全排列为例题,进行回溯算法的构建
3.1 首先用示例画出可能发生的情况,以及最终解,方便观察
3.2 然后通过观察上面图形,用四个问题整理思路,来构造递归树
分支探索
1.问:分支是如何产生的?如何探索分支?
答:.就对那几个数字进行查找,也就是在输入数组内进行组合,因此对数组进行遍历
10. for i to the length of the nums,i++ //进行路径探索
......
17. end for
确定答案形式
2. 问:题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从根结点到叶子结点的路径?
答:题目的解产生在从根结点到叶结点的路径,因此考虑用一个临时链表list保留当前路径,用一个链表answer保留已经探索过的路径(也就是题目的解)。
1. Initialize the list “answer” to save the path,which have been explored
2.