目录
去年忙各种事情一直断更到现在,话不多说,今天来看回溯算法
算法简介
回溯算法,也可以称为回溯搜索法,是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化来丢弃该解,即“回溯”并尝试另一个可能的解。这个过程一直进行到找到所有解或确定无解为止。
一个通过往返交通方式选择解释回溯思想的小示例:【回溯算法——跟着皇后学回溯】 https://www.bilibili.com/video/BV1c8411776k/?share_source=copy_web&vd_source=2e72b56d6a26d60223e77e7009d0535e
回溯算法的基本框架包括以下几个步骤:
- 定义问题的解空间:这是第一步,也是最重要的一步。解空间包含了所有可能的解。
- 确定易于搜索的解空间结构:选择一种合适的数据结构或表示方法,以便于有效地搜索解空间。
- 以深度优先搜索的策略搜索解空间:从根节点开始,按照深度优先的方式搜索解空间树。在搜索的过程中,可能会遇到需要决策的情况,这时会生成多个分支,每个分支代表一种可能的解。
- 在搜索过程中用剪枝函数避免无效搜索:这是提高算法效率的关键。当搜索到某个节点时,如果发现该节点不满足问题的约束条件(即该节点不可能产生解),则剪去该节点及其所有子节点,避免无效搜索。
基本算法框架如下:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表) (递归步骤)
撤销选择(回溯步骤)
关键在于理解这里的递归,当满足结束条件时,控制流结束递归来到回溯步骤,然后开始新一轮的for循环,具体如下图:
回溯算法可以解决多种类型的问题,包括但不限于:
- 组合问题:在N个数中按一定规则找出k个数的集合。
- 切割问题:一个字符串按一定规则有几种切割方式。
- 子集问题:一个N个数的集合里有多少符合条件的子集。
- 排列问题:N个数按一定规则全排列,有几种排列方式。
- 棋盘问题:如N皇后问题、解数独等。
此外,回溯算法还可以应用于图论中的路径问题,如判断在一个矩阵中是否存在一条包含某字符串所有字符的路径等。今天先看一道比较简单的组合问题~~
网站原题
(组合问题)力扣:77. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
分析解答
先用 n=4, k=2 这样不太大的数举例,使用回溯算法的程序控制流如下:
---以下分析基于力扣评论区的大神题解
用树形结构表达该算法的搜索过程如下:
下面是不考虑剪枝时的基本解法:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
# List[List[int]] 表示这个函数应该返回一个列表,其中每个元素是一个列表,列表中元素是整数
result, track = [], []
self.backtrack(n, k, 1, track, result)
return result
def backtrack(n, k, start, track, result):
if len(track) == k:
result.append(track[:])
return # 一旦找到组合,返回当前调用,但不影响外部循环
for i in range(start, n+1):
track.append(i)
backtrack(n, k, i+1, track, result)
track.pop() # 回溯,移除最后一个元素,准备尝试下一个元素
注意:对于第10行,新手小白常容易出现的一个错误是写成 result.append(track),输出结果总是嵌套空列表,这是因为
result.append(track)
实际上将列表变量【track
】的引用添加到result
列表中,而不是变量【track】存储的
列表数据。这意味着当你修改track
时,result
中的元素也会随之改变。所以正确的写法应该是:result.append(track[:])
或者:
result.append(track.copy())
另外复习一下,如果想要将
track
中的每个元素分别添加到result
中,而不是将track
作为一个整体列表添加进去时,可以使用extend
方法:result.extend(track)
将得到类似下面的结果:
[1,2,1,3,1,4,2,3,2,4,3,4]
接下来加入剪枝:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result, track = [], []
self.backtrack(n, k, 1, track, result)
return result
def backtrack(self, n, k, start, track, result):
if k == 0:
result.append(track[:])
return
for i in range(start, n-k+2):
track.append(i)
self.backtrack(n, k-1, i+1, track, result)
track.pop()
可以看到关键的剪枝步骤在这两行:
self.backtrack(n, k-1, i+1, track, result)
for i in range(start, n-k+2):
注:n-k+2 = n+1 - (k-1)
或者用一种更直观的剪枝策略:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result, track = [], []
self.backtrack(n, k, 1, track, result)
return result
def backtrack(self, n, k, start, track, result):
# 剪枝:如果当前track长度加上剩余可选元素数量小于k,则无需继续递归
if len(track) + (n + 1- start) < k:
return
if len(track) == k:
result.append(track[:])
return
for i in range(start, n+1):
track.append(i)
self.backtrack(n, k, i+1, track, result)
track.pop()