回溯法详解
作者:互联网
一、模板格式
回溯法问题实际上是一个决策树的遍历过程。可以分为三个部分:
1、路径:也就是已经做出的选择。
2、选择列表:也就是当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
回溯法不好理解的地方应该在撤销选择这一步,回溯会沿着一条路径走到结束状态,到这一步之后,需要返回到上一状态,这时候就需要执行撤销操作。
result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
二、示例讲解
1、全排列
""" 给定一个没有重复数字的序列,返回其所有可能的全排列。 示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] """ class Solution: def permute(self, nums): result = [] path = [] self.back_trace(nums, result, path) return result def back_trace(self, nums, result, path): """ 用递归来回溯 :param nums: :param result: :param path: :return: """ if len(path) == 3: # 满足结束条件 result.append(path[:]) return for i in range(len(nums)): path.append(nums[i]) # 做选择
# 全排序中可选择的列表是除了当前节点外的所有节点,所以是num[:i] + nums[i+1:] self.back_trace(nums[:i] + nums[i+1:], result, path) path.pop() # 撤销选择
2、全排列II
在包含重复数字的情况下,返回不重复的全列表。核心在于如何去重,即遍历是做剪枝操作,减少复杂度。为确保重复的数字不会重复组合,则需要对每个重复的数字标记一个顺序,并保证他们的相对位置不变,举例对于3个1,可以标记为1a、1b、1c。如果执行时1a没有被选择的话1b是不可以被选择的。这样就能避免重复。所以第一步对数组排序,确定好重复的数字的顺序。"""给定一个可包含重复数字的序列,返回所有不重复的全排列示例:
输入: [1,1,2] 输出: [ [1,1,2], [1,2,1], [2,1,1] ] """ import copy class Solution: def permuteUnique(self, nums): result = [] path = [] nums.sort() # 确定好重复数字的顺序 self.back_trace(nums, result, path, len(nums)) return result def back_trace(self, nums, result, path, length): if len(path) == 3: result.append(path[:]) return for i in range(len(nums)):
# 这一步是关键,以[1, 1, 2]为例,第一步选择第一个 1 之后,因为i = 0不会触发continue,而剩余的[1,2]没有重复,所以会得到[1] + [1, 2] = [1, 1, 2]
# 和[1] + [2, 1] = [1, 2, 1]。当遍历到第二个 1 时满足下面的条件会触发continue,所以会直接跳到 2,而剩余的[1, 1]有重复,和上面一样分析只会得到[1, 1],
# 最终就是 [2] + [1, 1] = [2, 1, 1]
if i > 0 and nums[i] == nums[i-1]: continue path.append(nums[i]) self.back_trace(nums[:i] + nums[i+1:], result, path, length) path.pop()
3、子集
子集和全排列有两个不同的地方,一是没有终止条件,即所有状态下的结果都要;二是有顺序的,不可扰乱。
""" 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ] """ class Solution: def subsets(self, nums): result = [] path = [] self.back_trace(nums, result, path) return result def back_trace(self, nums, result, path): result.append(path[:]) # 保存任意状态下的路径 for i in range(len(nums)): path.append(nums[i])
# num[i+1:] 表示不可扰乱数组的顺序,只能取当前状态后的元素 self.back_trace(nums[i+1:], result, path) path.pop()
4、子集II
同全排列II,对重复的元素排序,保证它们之间的相对位置。
""" 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: 输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ] """ class Solution: def subsetsWithDup(self, nums): result = [] path = [] nums.sort() # 一定要排序,因为是要按照后面是否和前面重复来筛选的,所以要将相同的元素排列在一起 self.back_trace(nums, result, path) return result def back_trace(self, nums, result, path): result.append(path[:]) for i in range(len(nums)): if i > 0 and nums[i] == nums[i - 1]: continue path.append(nums[i]) self.back_trace(nums[i + 1:], result, path) path.pop()
5、组合
组合中要注意三个点:一是终止条件是选择的元素相加等于target;二是如果当前选择的元素和大于target,需要剪枝。三是每个元素可以重复使用。
""" 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 说明: 所有数字(包括 target)都是正整数。 解集不能包含重复的组合。 示例 1: 输入: candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ] 示例 2: 输入: candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ] """ class Solution: def combinationSum(self, candidates, target: int): result = [] path = [] self.back_trace(candidates, result, path, target) return result def back_trace(self, nums, result, path, target):
# 满足终止条件 if sum(path) == target: result.append(path[:]) return for i in range(len(nums)):
# 满足剪枝的条件 if sum(path) + nums[i] > target: continue path.append(nums[i])
# num[i:] 是确保元素可以重复使用,不同于子集中的num[i+1:] self.back_trace(nums[i:], result, path, target) path.pop()
6、组合总数II
重复元素的问题同上全排列II和子集II。
""" 给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。 示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ] 示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ] """ class Solution: def combinationSum2(self, candidates, target: int): result = [] path = [] candidates.sort() # 排序保证重复元素的相对位置 self.back_trace(candidates, result, path, target) return result def back_trace(self, nums, result, path, target): if sum(path) == target: result.append(path[:]) return for i in range(len(nums)):
# 多增加一个重复元素的剪枝条件 if sum(path) + nums[i] > target or (i > 0 and nums[i] == nums[i - 1]): continue path.append(nums[i]) self.back_trace(nums[i + 1:], result, path, target) path.pop()
7、括号生成
括号要有效的前提是右括号要在左括号的右边,所以可以先选择左括号,再选择右括号,并且保证已经选择的左括号的个数是要大于或等于右括号的。
""" 给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。 例如,给出 n = 3,生成结果为: [ "((()))", "(()())", "(())()", "()(())", "()()()" ] """ class Solution: def generateParenthesis(self, n: int): result = [] path = ""
# left 和 right用来记录左括号和右括号的数量 self.back_trace(n, result, path, left=0, right=0) return result def back_trace(self, n, result, path, left, right):
# 终止条件 if len(path) == 2 * n: result.append(path)
# 先选择左括号 if left < n: self.back_trace(n, result, path + "(", left + 1, right)
# 选择右括号时保证右括号的数量小于左括号 if right < left: self.back_trace(n, result, path + ")", left, right + 1)
8、电话号码的字母组合
本道题是一个二维的遍历,先遍历数字列表,再遍历每个数字对应的字母列表
"""
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
"""
class Solution:
d = {"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz"}
def letterCombinations(self, digits: str):
result = []
path = []
self.back_trace(digits, result, path, len(digits))
return result
def back_trace(self, digits, result, path, length):
if len(path) == length:
result.append("".join(path))
return
for i in range(len(digits)):
for j in range(len(self.d[digits[i]])):
path.append(self.d[digits[i]][j])
self.back_trace(digits[i + 1:], result, path, length)
path.pop()
9、八皇后问题
有一个 8x8 的棋盘,往里放 8 个棋子,每个棋子所在的行、列、对角线都不能有另一个棋子。找出有多少种排列?
八皇后的核心问题是如何判断冲突的问题,在摆放棋子的时候是按行逐个排列的,所以行不会冲突,但是列、对角线(左上、右上两条对角线)会有可能冲突,所以需要把这三个已发生的状态存储下来,对新的皇后选择时要判断和已选的是否冲突,冲突则直接跳过。
class Solution: def __init__(self): self.result = 0 self.columns = set() # 存储已经选择过的列 self.diag_left = set() # 存储已经选择过的左上角的元素 self.diag_right = set() # 存储已经选择过的右上角的元素 def is_conflict(self, row, col): if col in self.columns or row - col in self.diag_left or row + col in self.diag_right: return True return False def eightQueen(self, n): self.dfs(0, n) return self.result def dfs(self, row, n): # 终止条件 if row == n: self.result += 1 return for col in range(n): # 剪枝 if self.is_conflict(row, col): continue # 执行选择,保存状态 self.columns.add(col) self.diag_left.add(row - col) self.diag_right.add(row + col) # 回溯 self.dfs(row + 1, n) # 撤销选择,删除状态 self.columns.remove(col) self.diag_left.remove(row - col) self.diag_right.remove(row + col)
标签:trace,nums,self,back,详解,result,回溯,path 来源: https://www.cnblogs.com/jiangxinyang/p/16415540.html