回溯算法
回溯算法:一种能避免不必要搜索的穷举式的搜索算法。采用试错的思想,在搜索尝试过程中寻找问题的解,当探索到其一步时,发现原先的选择并不满足求解条件,或者还需要满足更多求解条件时,就退回上一步重新选择,这种走不通就退回再走的技术称为「回溯法」,而满足回溯条件的某个状态的点称为「回溯点」。
回溯算法通常用简单的递归方法来实现,在进行回溯过程中更可能会出现两种情况:
- 找到一个可能存在的正确答案。
- 在尝试了所有可能的分布方法之后宣布该问题没有答案。
从全排列问题开始理解回溯算法
以求解[1, 2, 3]的全排列为例
从全排列的决策树中我们可以看出:
- 每一层中有一个或多个不同的节点,这些节点以及节点所连接的分支代表了「不同的选择」。
- 每一个节点代表了求解全排列问题的一个「状态」,这些状态是通过「不同的值」来表现的。
- 每向下递推一层就是在「可选元素列表」中选择一个「元素」加入到「当前状态」。 当一个决策分支探索完成之后,会逐层向上进行回溯。
- 每向上回溯一层,就是把所选择的「元素」从「当前状态」中移除,回退到没有选择该元素时的状态(或者说重置状态),从而进行其他分支的探索。
根据上文的思路和决策树,我们来写一下全排列的回溯算法代码(假设给定数组 nums中不存在重复元素)。则代码如下所示:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = [] # 存放所有符合条件结果的集合
path = [] # 存放当前符合条件的结果
def backtracking(nums): # nums 为选择元素列表
if len(path) == len(nums): # 说明找到了一组符合条件的结果
res.append(path[:]) # 将当前符合条件的结果放入集合中
return
for i in range(len(nums)): # 枚举可选元素列表
if nums[i] not in path: # 从当前路径中没有出现的数字中选择
path.append(nums[i]) # 选择元素
backtracking(nums) # 递归搜索
path.pop() # 撤销选择
backtracking(nums)
return res
回溯算法三步走
- 根据所给问题,定义问题的解空间:要定义合适的解空间,包括解的组织形式和显约束。
解的组织形式:将解的组织形式都规范为⼀个 n n n元组 x 1 , x 2 , . . . x n x_1,x_2,...x_n x1,x2,...xn。
显约束:对解分量的取值范围的限定,可以控制解空间的大小。
- 确定解空间的组织结构:解空间的组织结构通常以解空间树的方式形象地表达,根据解空间树的不同,解空间分为⼦集树、排列树、 m m m 叉树等。
- 搜索解空间:按照深度优先搜索策略,根据隐约束(约束函数和限界函数),在解空间中搜索问题的可⾏解或最优解。当发现当 前节点不满⾜求解条件时,就回溯,尝试其他路径。
如果问题只是求可⾏解,则只需设定约束函数即可,如果要求最优解,则需要设定约束函数和限界函数。
回溯算法的基本思想是:以深度优先搜索的方式,根据产生子节点的条件约束,搜索问题的解。当发现当前节点已不满足求解条件时,就「回溯」返回,尝试其他的路径。
在写回溯算法时,我们可以按照这个思路来书写,具体步骤如下:
- 明确所有选择:画出搜索过程的决策树,根据决策树来确定搜索路径。
- 明确终止条件:推敲出递归的终止条件,以及递归终止时的要执行的处理方法。
- 将决策树和终止条件翻译成代码:定义回溯函数、书写回溯函数主体、明确递归终止条件
回溯算法的应用
子集
题目大意:给定一个整数数组
n
u
m
s
nums
nums,数组中的元素互不相同。返回该数组所有可能的不重复子集。可以按任意顺序返回解集。
示例:
输入 nums = [1,2,3]
输出 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
解题思路:可以通过向当前子集数组中添加可选元素来表示选择该元素。也可以在当前递归结束之后,将之前添加的元素从当前子集数组中移除来表示不选择该元素。
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = [] # 存放所有符合条件结果的集合
path = [] # 存放当前符合条件的结果
def backtracking(nums, index): # 正在考虑可选元素列表中第 index 个元素
res.append(path[:]) # 将当前符合条件的结果放入集合中
if index >= len(nums): # 遇到终止条件(本题)
return
for i in range(index, len(nums)): # 枚举可选元素列表
path.append(nums[i]) # 选择元素
backtracking(nums, i + 1) # 递归搜索
path.pop() # 撤销选择
backtracking(nums, 0)
return res
print(Solution().subsets([1,2,3]))
N 皇后
题目大意:给定一个整数
n
n
n。返回所有不同的「
n
n
n皇后问题」的解决方案。每一种解法包含一个不同的「
n
n
n皇后问题」的棋子放置方案,该方案中的Q和.分别代表了皇后和空位。
示例:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如下图所示,4 皇后问题存在 2 个不同的解法。
解题思路:这道题是经典的回溯问题。我们可以按照行序来放置皇后,也就是先放第一行,再犯第二行…直到最后一行。对于
n
∗
n
n*n
n∗n的棋盘来说,每一列有
n
n
n中放法可供选择。我们可以尝试选择其中一列,查看是否与之前放置的皇后有冲突,如果没有冲突,则继续在下一行放置皇后,依次类推,直到放置完所有皇后,并且都不发生冲突时,就得到了一个合理的解。
from typing import List
class Solution:
res = []
def backtrack(self, n: int, row: int, chessboard: List[List[str]]):
if row == n:
temp_res = []
for temp in chessboard:
temp_str = ''.join(temp)
temp_res.append(temp_str)
self.res.append(temp_res)
return
for col in range(n): # 枚举可放置皇后的列
if self.isValid(n, row, col, chessboard): # 如果该位置与之前放置的皇后不发生冲突
chessboard[row][col] = 'Q' # 选择row,col位置放置皇后
self.backtrack(n, row + 1, chessboard) # 递归放置row + 1行之后的皇后
chessboard[row][col] = '.' # 撤销选择 row,col位置
# 判断当前未知row,col是否与之前放置的皇后发生冲突
def isValid(self, n: int, row: int, col: int, chessboard: List[List[str]]):
for i in range(row):
if chessboard[i][col] == 'Q':
return False
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if chessboard[i][j] == 'Q':
return False
i -= 1
j -= 1
i, j = row - 1, col + 1
while i >= 0 and j < n:
if chessboard[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def solveNQueens(self, n: int) -> List[List[str]]:
self.res.clear()
chessboard = [['.' for _ in range(n)] for _ in range(n)]
self.backtrack(n, 0, chessboard)
return self.res
print(Solution().solveNQueens(5))
电话号码的字母组合
题目大意:给定一个只包含数字2~9的字符串digits。给出数字到字母的映射。注意1不对应任何字母。返回字符串digits在九宫格键盘上所能表示的所有字母组合。
示例:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
解题思路:用哈希表保存每个数字键位对应的所有可能的字母,然后进行回溯操作。回溯过程中,维护一个字符串 combination,表示当前的字母排列组合。初始字符串为空,每次取电话号码的一位数字,从哈希表中取出该数字所对应的所有字母,并将其中一个插入到 combination 后面,然后继续处理下一个数字,知道处理完所有数字,得到一个完整的字母排列。开始进行回退操作,遍历其余的字母排列。
from typing import List
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
phone_dict = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz"
}
def backtrack(combination, index):
if index == len(digits):
combinations.append(combination)
else:
digit = digits[index]
for letter in phone_dict[digit]:
backtrack(combination + letter, index + 1)
combinations = list()
backtrack('', 0)
return combinations
print(Solution().letterCombinations("23"))