1. 算法背景
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
2. 解决问题的一般步骤
- 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
- 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
- 以深度优先的方式搜索解空间,并且在搜索过程中用
剪枝函数避免无效搜索。
3. 回溯算法框架
实际上是穷举的过程,代码的递归形式中主要体现为做选择和撤销选择
决策出口,即何时觉得可以输出到结果中了。
如何做出选择,从待选列表nums中去选择元素了,可能会需要nums排序,或在有重复数字的时候进行剪枝。
result = []
def _backtrace(选择列表nums, 路径pre_list):
if 满足结束条件:
result.append(路径)
return
for 选择 in 选择列表:
做选择
_backtrace(剩余选择列表, 路径)
撤销选择
4. 适合算法的例子
4.1 全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。-有序排列
def permute(nums):
if len(nums) == 0:
return []
res = []
def _backtrace(arr: list, pre_list: list):
# 递归出口,满足条件 加入数组
if len(arr) == 0:
# 这里需要进行深拷贝 不然会记录会根据pre_list改变 最后导致全部为空列表
res.append(pre_list[:])
return
for i in range(len(arr)):
# 1. 做选择-> 开始进行一些根据题意的判断等
pre_list.append(arr[i])
left_list = arr[:]
left_list.remove(arr[i])
# 2. 递归做出剩余选择->需要判断左侧新的数组从哪个位置开始
_backtrace(left_list, pre_list)
# 3. 撤销之前使用过的选择 -> 把当前值移出路径,才能进入下一个值的路径
pre_list.pop()
_backtrace(nums, [])
return res
if __name__ == '__main__':
print(permute([1, 2, 3]))
# 输入 [1,2,3] 输出 [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
4.2 全排列 II
给定一个可包含重复数字的序列,返回所有不重复的全排列
def permute(nums):
if len(nums) == 0:
return []
res = []
def _backtrace(arr: list, pre_list: list):
if len(arr) == 0:
res.append(pre_list[:])
return
for i in range(len(arr)):
# # 当前元素和上一个元素一样,不用重复加入了,进行剪枝
if i > 0 and arr[i] == arr[i - 1]:
continue
pre_list.append(arr[i])
left_list = arr[:]
left_list.remove(arr[i])
_backtrace(left_list, pre_list)
pre_list.pop()
_backtrace(nums, [])
return res
if __name__ == '__main__':
print(permute([1, 2, 2]))
# 输出 [[1, 2, 2], [2, 1, 2], [2, 2, 1]]
4.3 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集,解集不能包含重复的子集。
决策出口:这道题是只要进入回溯函数,就输出到结果中,因为没有要求当前的路径元素个数的限制。2)如何做出选择:按顺序从nums中加入到pre_list中即可
def subsets(nums: list):
if len(nums) == 0:
return []
nums.sort()
res = []
def _backtrace(arr: list, pre_list: list):
res.append(pre_list[:])
for i in range(len(arr)):
# if i > 0 and arr[i] == arr[i - 1]:
# continue
pre_list.append(arr[i])
_backtrace(arr[i + 1:], pre_list)
pre_list.pop()
_backtrace(nums, [])
return res
if __name__ == '__main__':
print(subsets([1, 2, 3]))
# 输出:[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
4.4 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。
def subsets(nums: list):
if len(nums) == 0:
return []
nums.sort()
res = []
def _backtrace(arr: list, pre_list: list):
res.append(pre_list[:])
for i in range(len(arr)):
if i > 0 and arr[i] == arr[i - 1]:
continue
pre_list.append(arr[i])
_backtrace(arr[i + 1:], pre_list)
pre_list.pop()
_backtrace(nums, [])
return res
if __name__ == '__main__':
print(subsets([1, 2, 2]))
4.5 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。
1)决策出口:当pre_list路径中所有数之和等于target,输出到res中。
2)做出选择:由于每个数字可以使用多次,所以nums就是原数组。此外,这里可以使用剪枝来降低复杂度——对原数组排序,若从待选列表中加入当前数之后的总和大于target,那么该数之后的元素都不需要考虑了。
def combination_sum(nums: list, target: int):
if len(nums) == 0:
return []
nums.sort()
res = []
def _backtrace(arr: list, pre_list: list):
if sum(pre_list) == target:
res.append(pre_list[:])
return
for i in range(len(arr)):
left_num = target - arr[i] - sum(pre_list)
# 剪枝
if left_num < 0:
break
pre_list.append(arr[i])
# arr更新为该数及该数之后
_backtrace(arr[i:], pre_list)
# 把当前值移出路径,才能进入下一个值的路径
pre_list.pop()
_backtrace(nums, [])
return res
if __name__ == '__main__':
print(combination_sum([2, 3, 6, 7], 7))
# 输出 :[[2, 2, 3], [7]]
4.6 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。