理解回溯算法——从全排列问题开始

发布时间 2023-04-08 21:42:06作者: blogzzt

一、简介

回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状
态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。

 

二、从全排列问题开始理解回溯算法
以数组 [1, 2, 3] 的全排列为例。

先写以 1开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列(注意:递归结构体现在这里);
再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;
最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。
总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。

看到这里的朋友,建议先尝试自己画出「全排列」问题的树形结构。

 

 

 

说明:

1、每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
2、使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」
3、深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
4、深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果。
使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。

 

三、题解

46. 全排列

 1 class Solution:
 2     def permute(self, nums: List[int]) -> List[List[int]]:
 3         def dfs(nums, size, depth, path, used, res):
 4             if depth == size:
 5                 res.append(path[:])  # 注意append方法是浅拷贝,如果用res.append(path)是不行的,最后输出的都是[]
 6                 return
 7 
 8             # 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
 9             for i in range(size):
10                 if not used[i]:
11                     used[i] = True
12                     path.append(nums[i])
13 
14                     dfs(nums, size, depth + 1, path, used, res)
15 
16                     # 注意:下面这两行代码发生 「回溯」(需要做「状态重置」,即「回到过去」)
17                     # 回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
18                     used[i] = False
19                     path.pop()
20 
21         size = len(nums)
22         if len(nums) == 0:
23             return []
24 
25         used = [False for _ in range(size)]
26         res = []
27         dfs(nums, size, 0, [], used, res)
28         return res