![a5f4f992035b6b647fc66afbbaef89bd.png](https://i-blog.csdnimg.cn/blog_migrate/240e3de8ff5857a44315382d850bd26f.png)
回溯算法顾名思义就是有回溯的算法呗~(当然这是句废话,只是缓解一下我第一次写知乎文章无从下笔的尴尬)。入正题 ↓
不知道看到这篇文章的小伙伴有没有看过一个之前大火的动漫《从零开始的异世界》,这部动漫里男主昂每次挂掉后都会从上个存档点开始新一轮的冒险,这就是一个简单的回溯思想——昂在不断的回溯中找到活下来的方法。回溯算法也具有同样的道理,往往来说,回溯算法先向下递归搜索一个可能解,然后通过不断地回溯到上一个“存档”在向下递归来找到所有满足条件的最优解。
其实,回溯思想有点类似于for循环暴力遍历所有解空间,唯一不同的地方是回溯算法往往使用递归来实现。而递归相比于for循环具有一个先天的优势,就是无需知道遍历的深度,所以能用for循环实现的一定能用递归实现,但是反过来可不一定成立哦~
多说无意,举个简单的例子,以方便直观的理解。
比如,经典的全排列问题(题目来自leetcode):
# 给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
咋一看这道题好像可以用for循环暴力解,但是如果你真的尝试使用for循环去实现全排列的话,会发现真的很难写(总会缺斤少两 ~),原因是遍历的深度会随着集合长度增加而增加。但是如果用回溯思想则可以很简单的实现。为了先构建起回溯算法的基本思路,这里以上面的示例为例,我们看看回溯算法是如何进行搜索的。
![434f3d0d9b00b73d5abd72527c62f670.png](https://i-blog.csdnimg.cn/blog_migrate/148261eb489b591ee258001a22a7b569.jpeg)
如上图所示,我们可以把全排列问题分为3个小部分:
- 以1为开头的所有排列 = [1] + [2,3]的全排列
- 以2为开头的所有排列 = [2] + [1,3]的全排列
- 以3为开头的所有排列 = [3] + [1,2]的全排列
每次向下递归都是在尚未访问元素中继续进行,如果递归触底则向上回溯到最近的一个元素接着向下递归。下面是Python写的全排列代码,读者可以参考。
class Solution(object):
def permute(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
length = len(nums)
res = []
# checked 数组用来回溯
def permute_dfs(numset , checked, deepth):
if deepth == length:
res.append(checked[:])
return
for i in range(length):
if numset[i] not in checked:
checked.append(numset[i]) # 先向下递归
permute_dfs(numset, checked , deepth+1)
checked.pop() # 回溯到上一个节点
permute_dfs(nums , [] , 0)
return res
这里回溯的实现是通过checked数组+递归来实现的,我们在递归中用checked数组记录访问的路径,每当节点的其中一个分支递归结束后,就把这个分支的路径清除,以便继续另一个分支,这就是回溯的思想。
leetcode上还有一些和全排列问题非常相似的回溯问题,如果这里的思想看明白了的话,推荐尝试做一做下面这些同类型的题目,以加固对回溯算法的理解。
全排列IIleetcode-cn.com 组合leetcode-cn.com 子集leetcode-cn.com回溯算法除了求解类似全排列这种整个解空间的问题之外,其实更多的场景是用在求最优解的问题上,通过暴力回溯遍历整个解空间以寻求我们需要的最优解。所以单从回溯过程来看,回溯算法并没有很高的效率,只是为我们解决问题提供了一种通用的解法。寻找最优解的问题往往是在回溯算法框架之上,加入了剪枝函数,通过提前把不满足条件的分支剪掉来减少搜索的次数,进而提升回溯算法的速度。
寻找最优解空间的回溯问题,放到下篇接着分享吧,这篇就到这里~。第一次写文章,希望大家多支持,有问题的地方可以戳评论区,感谢批评指正!