一、基本理解
回溯算法的基本思想是:以深度优先搜索的方式,根据产生子节点的条件约束,搜索问题的解。当发现当前节点已不满足求解条件时,就「回溯」返回,尝试其他的路径。
二、三步走方法
res = [] # 存放所欲符合条件结果的集合
path = [] # 存放当前符合条件的结果
def backtracking(nums): # nums 为选择元素列表
if 遇到边界条件: # 说明找到了一组符合条件的结果
res.append(path[:]) # 将当前符合条件的结果放入集合中
return
for i in range(len(nums)): # 枚举可选元素列表
path.append(nums[i]) # 选择元素
backtracking(nums) # 递归搜索
path.pop() # 撤销选择
backtracking(nums)
一、明确所有选择
- 画出决策树:决策树可以帮助理清搜索过程,确定搜索范围和路径。
- 确定搜索路径:根据决策树明确在不同阶段的所有可能选择。
二、明确终止条件
- 确定终止位置:回溯算法的终止条件通常是决策树的底层,即达到无法再做选择的状态。
- 处理终止情况:明确在终止条件下的处理方法,如输出答案、将当前符合条件的结果放入集合等。
三、将决策树和终止条件翻译成代码
- 定义回溯函数:
- 明确函数意义:理解该函数在整个回溯过程中的作用。
- 确定传入参数和全局变量:由递归搜索阶段的 “当前状态” 决定,最好能直接记录 “当前状态”,如全排列中,
backtracking(nums)
函数的参数nums
表示当前可选元素,全局变量res
和path
分别用于保存所有符合条件的结果和当前符合条件的结果。 - 确定返回结果:一般是单个节点或数值,告知上一层函数当前搜索结果,也可使用全局变量保存 “当前状态” 而不返回结果。
- 书写回溯函数主体:
- 枚举可选元素列表:通过循环遍历所有可能的选择。
- 约束条件判断:根据给定的约束条件筛选合法的选择。
- 选择元素:将满足条件的元素加入表示当前状态的变量中。
- 递归搜索:调用自身继续探索下一层。
- 撤销选择:在回溯时,将之前的选择撤销,尝试其他可能。
- 明确递归终止条件:
- 将终止条件转换为代码中的条件语句。
- 执行终止条件下的处理方法,如将结果存入集合等。
三、应用
1.全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
- 明确所有选择:
- 遍历输入的数组
nums
,对于每个元素,如果它不在当前路径path
中,就可以作为一个选择。 - 这确定了从数组中选择未被使用过的元素加入路径的所有可能性。
- 遍历输入的数组
- 明确终止条件:
- 当
path
的长度与输入数组nums
的长度相等时,表明找到了一个全排列,将其添加到结果列表res
中并返回。
- 当
- 将决策树和终止条件翻译成代码:
- 定义回溯函数
backtracking
,不需要传入结果集和当前路径,直接使用外部函数中的变量res
和path
。 - 在回溯函数主体中,通过循环遍历所有选择,判断元素是否在路径中,满足条件则进行选择、递归搜索和撤销选择的操作。
- 明确递归终止条件,当
path
长度满足要求时,将其加入结果集。
- 定义回溯函数
def permute(nums):
res = []
path = []
def backtracking(nums):
# 明确终止条件
if len(path) == len(nums):
res.append(path[:])
return
# 明确所有选择
for num in nums:
if num not in path:
# 选择元素
path.append(num)
# 递归搜索
backtracking(nums)
# 撤销选择
path.pop()
backtracking(nums)
return res
2.全排列Ⅱ
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
def permuteUnique(nums):
nums.sort()
res = []
path = []
used = [False] * len(nums)
def backtracking():
if len(path) == len(nums):
res.append(path[:])
return
for i in range(len(nums)):
if used[i] or (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]):
continue
path.append(nums[i])
used[i] = True
backtracking()
used[i] = False
path.pop()
backtracking()
return res
3.括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
分析:
一、明确所有选择
- 有两种选择,添加左括号 “(” 或者添加右括号 “)”。
- 但添加括号时有约束条件,不能无限制地添加,要保证生成的括号组合是有效的。
二、明确终止条件
当左括号和右括号的数量都等于给定的对数 n
时,说明找到了一个有效的括号组合,将其加入结果列表 res
。
三、将决策树和终止条件翻译成代码
- 定义回溯函数
backtracking
,传入当前已使用的左括号数量left
、右括号数量right
和当前生成的括号组合path
。 - 在回溯函数主体中,如果左括号数量小于
n
,可以添加左括号,递归调用函数继续生成。如果右括号数量小于左括号数量,可以添加右括号,递归调用函数。 - 明确递归终止条件,当左右括号数量都等于
n
时,将当前括号组合加入结果列表。
def generateParenthesis(n):
res = []
def backtracking(left, right, path):
if left == n and right == n:
res.append(path)
return
if left < n:
backtracking(left + 1, right, path + "(")
if right < left:
backtracking(left, right + 1, path + ")")
backtracking(0, 0, "")
return res
4.电话按键字母组合
一、明确所有选择
- 对于输入的数字字符串中的每个数字,都有对应的一组字母可供选择。
- 遍历每个数字对应的字母集合,确定所有可能的选择。
二、明确终止条件
当处理完数字字符串的最后一个数字,即索引达到字符串的长度时,说明找到了一个字母组合,将其加入结果列表 res
。
三、将决策树和终止条件翻译成代码
- 定义回溯函数
backtracking
,传入当前处理的数字字符串的索引。 - 在回溯函数主体中,如果当前索引等于数字字符串的长度,说明已经生成了一个完整的字母组合,将其加入结果列表。否则,获取当前数字对应的字母集合,遍历该集合,将每个字母加入当前路径,然后递归调用函数处理下一个数字。递归调用结束后,将当前加入的字母从路径中移除,进行回溯。
def letterCombinations(digits):
if not digits:
return []
mapping = {
'2': 'abc',
'3': 'def',
'4': 'ghi',
'5': 'jkl',
'6': 'mno',
'7': 'pqrs',
'8': 'tuv',
'9': 'wxyz'
}
res = []
path = ""
def backtracking(index):
if index == len(digits):
res.append(path)
return
letters = mapping[digits[index]]
for letter in letters:
path += letter
backtracking(index + 1)
path = path[:-1]
backtracking(0)
return res
5.组合总和
def combinationSum(candidates, target):
res = []
path = []
def backtracking(target, start):
if target == 0:
res.append(path[:])
return
if target < 0:
return
for i in range(start, len(candidates)):
path.append(candidates[i])
backtracking(target - candidates[i], i)
path.pop()
backtracking(target, 0)
return res
6.子集
def subsets(nums):
res = []
path = []
def backtracking(start):
res.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtracking(i + 1)
path.pop()
backtracking(0)
return res