回溯算法二三事
设计回溯的基本步骤
回溯和深度优先遍历有相通之处
回溯时,需要用一个栈保存状态变量path。在往下一步时添加选择path.append();在往上一步(“回头”)时,需要撤销上一次的选择path.pop()
设计状态变量:其他节点/叶子节点/根节点
需要注意:
-
回溯和DFS过程中,状态变量
path
所指向的列表在深度优先遍历的过程中只有一份,DFS完成后会返回根节点变为空。因此如果在传递参数的过程中不创建新的变量(比如直接传递path),就要善用import copy//res.append(copy.deepcopy(path))
或者res.append(path[:])
。 -
在python中
[1,2,3]+[4]
会创建一个新的列表对象,path[:]
也是。如果在DFS传递参数的过程中创建新的变量,则不用考虑上一条。但是会多用很多的额外空间。
为什么不是广度优先遍历?
- 深度优先遍历和广度优先遍历都可以遍历所以状态空间,在这一点上,用广度优先遍历是没问题的
- 深度优先遍历在不同状态之间的切换很容易,可以使用函数调用的系统栈。但是广度优先遍历中,从浅层转到深层,状态的变化很大。得用队列+节点类
做题先画树形图
做题时建议先画树形图,想清楚:
- 分支如何产生
- 题目需要的解在哪里?是在叶子节点、还是在非叶子节点、还是在从根节点到叶子结点的路径
- 那些搜索会产生不需要的解?比如:产生重复的原因是什么?可以怎么剪枝?
参考资料和例题:46. 全排列 - 力扣(LeetCode)
更进一步!DFS传递参数问题:
leetcode46.全排列,可以不需要传递参数
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [False] * len(nums)
def dfs(): # dfs中没有显示的return也没有关系
# 根本不需要局部变量?因为每个时刻只会有一种状态,所以只需要维护一份状态变量
# 题目需要的解
if len(path) == len(nums):
res.append(copy.deepcopy(path))
for i in range(len(nums)):
if used[i] is False:
path.append(nums[i])
used[i] = True
dfs()
path.pop()
used[i] = False
dfs()
return res
leetcode78.子集,由于DFS中不能修改循环使用的变量,所以必须传递一个参数i
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [False] * len(nums)
index = 0
def dfs(index):
# global index # 声明全局变量index
# 题目需要的解
if len(path) <= len(nums):
res.append(copy.deepcopy(path))
# print('path: {}'.format(path))
# print('index: {}'.format(index))
for i in range(index, len(nums)):
# 2.局部变量在定义前引用。不能在使用中修改变量index,就是此处的DFS必须传递一个参数index的原因
if used[i] is False:
path.append(nums[i])
used[i] = True
# index = index + 1 # 1.对于全局变量,只能引用而不能修改。如果要修改则会创建新的局部变量
dfs(index = i)
path.pop()
used[i] = False
# index = index - 1
# print('index: {}'.format(index))
dfs(index)
return res
leetcode494.目标和 回溯超时,倒着回溯用@cache当dp用空间换时间 具体原理见 python中的cache
# 模仿version dp+cache
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
@ cache
# 当dfs(index, res)对每一种(idnex,res)组合只会出现一种解,加上@cache之后会自动把算过的解存下来,之后要用到的时候首先去查表,没查到才会计算。是空间换时间的艺术。
def dfs(index, res):
if index == -1: # 全部的数都用完了,需要出循环
if res == 0: # 值刚好相等
return 1 # 提供一种可能性
return 0 # 值不等,不提供可能性
return dfs(index-1, res-nums[index]) + dfs(index-1, res+nums[index]) # 分别对应nums[index]前为加号或者减号的情况
return dfs(len(nums)-1, target)
# version 0,会超时
# 还要用dp?
# 用完表达式
# 当到叶子节点满足要求时,返回路径数。树的深度是nums的长度,每个节点有两个分叉+和-
# 剪枝:如果目前的值+之后的数[全负-全正] 的区间范围内没有target,这条路线就不用看了
class Solution:
res = 0
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# 特殊情况处理
path = 0 # 当前的值
depth = 0
def dfs(path, depth):
# print('path={}, depth={}'.format(path, depth))
# 返回条件
if depth == len(nums):
# print('depth == len(nums)-1')
if path == target:
# print('path == target')
self.res += 1
# print('self.res:{}'.format(self.res))
return
number = nums[depth]
# 剪枝
possible_max = path + sum(nums[depth:])
possible_min = path - sum(nums[depth:])
if target > possible_max or target < possible_min:
# print('cut off')
return
for i in [1, -1]:
path += i*number
depth += 1
# print('number={}, path={}, depth={}'.format(number, path, depth))
dfs(path, depth)
path -= i*number
depth -= 1
# print('START: path={}, depth={}'.format(path, depth))
dfs(path, depth)
return self.res