1.原理
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
2.操作
也就是说解决一个回溯问题,实际上就是一个决策树的遍历过程。在这个过程中只需要思考三个问题:
(1)路径:也就是已经做出的选择;
(2)选择列表:也就是你当前可以做的选择;
(3)结束条件:也就是1到达决策树底层,无法再做选择的条件
回溯算法框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
核心是for循环里面的递归,在递归调用之前做选择,在递归调用之后撤销选择。
3.例子
例一:
Leetcode算法题库里面的39题:
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
代码:
class Solution:
def combinationSum(self, candidates, target):
size = len(candidates)
candidates.sort()
print(candidates[0])
res = []
path = []
self.backtrack(res, candidates, target, path, size)
return res
def backtrack(self, res, candidates, target, path, size):
if target == 0:
x = path[:]
x.sort()
if x not in res:
res.append(path[:])
return
for index in range(0, size):
residue = target - candidates[index]
if residue < 0: # 剪枝操作,优化代码
break
path.append(candidates[index])
self.backtrack(res, candidates, residue, path, size)
path.pop()
if __name__ == '__main__':
candidates = [2, 3, 6, 7]
target = 7
solution = Solution()
result = solution.combinationSum(candidates, target)
print(result)
结果:
[[2, 2, 3], [7]]
其中路径是path,选择列表是candidates,结束条件是 if target == 0,做选择用了一个append,撤销选择用了一个出栈的操作。因为candidates中的数据可以无限重复选取,所以每次递归的选择列表都不变。
例二:
Leetcode算法题库里面的46题:
代码:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def backtrack(path, nums):
# 如果满足满足结束条件则添加路径
if len(path) == n:
path_s = path.copy()
result.append(path_s)
return
for i in range(0, n):
if nums[i] not in path: #每个数字只能用一次
# 路径选择
path.append(nums[i])
# 回溯
backtrack(path, nums)
# 撤销路径
path.pop()
result = []
n = len(nums)
path = []
backtrack(path, nums)
return result
例三:
[Leetcode算法题库里面的47题
代码:
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
def backtrack():
# 如果满足满足结束条件则添加路径
if len(path) == n and path not in result:
path_s = path.copy()
result.append(path_s)
return
for i in range(0, n):
# 路径选择
# 设置index可以保证不做重复选择
if i not in index:
index.append(i)
path.append(nums[i])
else:
continue
# 回溯
backtrack()
# 撤销选择
path.pop()
index.pop()
result = []
n = len(nums)
index = []
path = []
backtrack()
return result
例四:
def subsets(nums):
def backtrack(first, path):
if len(path) == k:
path_s = path.copy()
result.append(path_s)
return
for i in range(first, n):
if nums[i] not in path:
# 是否要对第i+1个item进行回溯,前面的i个item已经判断完了,不用管
path.append(nums[i])
backtrack(i+1, path)
path.pop()
n = len(nums)
path = []
result = []
# 表示从nums中取k个item
for k in range(n+1):
backtrack(0, path)
return result
nums = [1,2,3]
print(subsets(nums))
例五
"practice_17.py" 28L, 791C
def letterCombinations(digits):
def backtrack(index, path):
if n == 0:
return []
if len(path) == n:
result.append(''.join(path[:]))
return
for letter in digit2letter_dict[digits[index]]:
path.append(letter)
backtrack(index+1, path)
path.pop()
digit2letter_dict = {'2':['a', 'b', 'c'], '3':['d', 'e', 'f'],
'4':['g', 'h', 'i'], '5':['j', 'k', 'l'],
'6':['m', 'n', 'o'], '7':['p', 'q', 'r', 's'],
'8':['t', 'u', 'v'], '9':['w', 'x', 'y', 'z']}
n = len(digits)
path = []
result = []
backtrack(0, path)
return result
digits = "23"
# digits = "2"
digits = ""
print(letterCombinations(digits))