前言
今天继续带大家进行回溯的实战篇3,去学习如何用回溯的方法去解决排列和棋盘以及其他用回溯方法解决的问题,最重要的就是学会回溯三部曲的构建,一文带大家弄懂。本文用于记录自己的学习过程,同时向大家进行分享相关的内容。本文内容参考于代码随想录同时包含了自己的许多学习思考过程,如果有错误的地方欢迎批评指正!
排列
全排列
相关技巧:来看看怎么用回溯法做全排列的问题,首先我们要知道什么是全排列,简单说就是排列出不同顺序的组合,每个数都出现且只出现一次。那就看怎么用回溯了,我们想之前做组合的时候都是选一个从选的这个数的后面剩下的来选,但是做全排列要从所有的数来选就是选了第二个数,还有可能选择第一个数,不像组合,选了第二个数就从第二个数后面的剩下的来选了。那么这里我们就不在需要startindex来标记我们选到哪里, 因为我们要从全集来选。然后我们要怎么确定每个数只选一次呢,还是用个used数组,代表该数是否使用过,使用过就下一个。这么说看看图,你应该就能很快理解了。
- 确定回溯的参数和返回值:我们来看回溯参数,首先就是nums肯定是需要的,然后我们还需要个used数组用来查看我们的数是否使用过。还有我们需要有一个参数path来存储我们的路径,用来加入新的和弹出操作,最后当结果符合,我们肯定需要一个result来保存我们的结果。至于返回值就不需要了,因为我们有result参数来保存我们的结果了。
- 确定回溯的终止条件:当路径的长度等于数组的长度就代表我们找到了一个全排列了,加入结果集退出单层回溯。
- 确定单层回溯的逻辑:首先判断是否到了回溯的终止条件,没有就继续单层回溯过程,for循环搜索整个nums,然后就是used判断是否使用过,接着就是基础的回溯过程了。
class Solution:
def permute(self, nums):
result = []
self.backtracking(nums, [], [False] * len(nums), result)
return result
def backtracking(self, nums, path, used, result):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
path.append(nums[i])
self.backtracking(nums, path, used, result)
path.pop()
used[i] = False
全排列II
相关技巧:这里又出现了重复的元素,其实本质跟组合总和II和子集II的问题一样,都是去重,所以我们这里还是会用到used数组,这时候used就会起到两个作用,不仅仅是标记该元素是否使用过,同样也是执行着去重的作用。所以就是在全排列的题目基础上,判断是否使用过的时候也去判断实现去重功能,即重复的元素同树枝可出现,同层不可出现。
- 确定回溯的参数和返回值:我们来看回溯参数,首先就是nums肯定是需要的,然后我们还需要个used数组用来查看我们的数是否使用过,同样的used数组也会帮我们实现去重的功能,具体的如何实现的可以去看组合那篇有讲过。还有我们需要有一个参数path来存储我们的路径,用来加入新的和弹出操作,最后当结果符合,我们肯定需要一个result来保存我们的结果。至于返回值就不需要了,因为我们有result参数来保存我们的结果了。
- 确定回溯的终止条件:当路径的长度等于数组的长度就代表我们找到了一个全排列了,加入结果集退出单层回溯。
- 确定单层回溯的逻辑:首先判断是否到了回溯的终止条件,没有就继续单层回溯过程,for循环搜索整个nums,然后就是used判断是否使用过,同样进行去重,接着就是基础的回溯过程了。
class Solution:
def permuteUnique(self, nums):
nums.sort() # 排序
result = []
self.backtracking(nums, [], [False] * len(nums), result)
return result
def backtracking(self, nums, path, used, result):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]:
continue
used[i] = True
path.append(nums[i])
self.backtracking(nums, path, used, result)
path.pop()
used[i] = False
棋盘问题
N皇后
相关技巧:N皇后是一个很经典的问题了,什么是N皇后问题呢?就是N个皇后在NxN的棋盘中每个占据的一个格子,同时其每个皇后的同行同列和其所在的斜线上不能够有别的皇后,就是这个意思。正常的来解是很复杂的,我们来看回溯怎么来解决。我们之前都是一维的,现在N皇后问题来到了二维的,其实原理都是一样的,之前看作树来解决,同样的我们现在还是来看做数,看成一颗特殊的树结构,行就是他的高度,列就是每层的节点数量,每层都是n个节点,高度为n。
然后再来思考,我们每层只放一个皇后,放完一个皇后就要判断其是否位置合法,考虑三个情况:检查列是否存在皇后、检查 45 度角是否有皇后、检查 135 度角是否有皇后,不需要考虑同行的情况,因为我们每次在一行就放一个,就下一行了,并且回溯也会去掉在同行放过的皇后。所以就很简单了,放一次判断一次,直到到最后一层,回溯结束,加入结果集。
- 确定回溯的参数和返回值:首先就是我们的棋盘chessboard,其实其相当于之前我们回溯问题中path的作用,和存放我们的结果集result,还有表示到了什么位置的row,代表总行数的n
- 确定回溯的终止条件:当row=n的时候,就是说明我们到了最后的叶子节点了。将结果加入结果集即可。
- 确定单层回溯的逻辑:首先判断是否到最后一层了,没有就继续当前的回溯过程,从当前的位置开始往后放皇后,放完之后,判读当前的皇后是否是合法的,合法的话就进入回溯继续再下一行放皇后,不是合法的就往后再放皇后直到皇后位置合法,或者到最后为止。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
result = [] # 存储最终结果的二维字符串数组
chessboard = ['.' * n for _ in range(n)] # 初始化棋盘
self.backtracking(n, 0, chessboard, result) # 回溯求解
return [[''.join(row) for row in solution] for solution in result] # 返回结果集
def backtracking(self, n: int, row: int, chessboard: List[str], result: List[List[str]]) -> None:
if row == n:
result.append(chessboard[:]) # 棋盘填满,将当前解加入结果集
return
for col in range(n):
if self.isValid(row, col, chessboard):
chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1:] # 放置皇后
self.backtracking(n, row + 1, chessboard, result) # 递归到下一行
chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1:] # 回溯,撤销当前位置的皇后
def isValid(self, row: int, col: int, chessboard: List[str]) -> bool:
# 检查列
for i in range(row):
if chessboard[i][col] == 'Q':
return False # 当前列已经存在皇后,不合法
# 检查 45 度角是否有皇后
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if chessboard[i][j] == 'Q':
return False # 左上方向已经存在皇后,不合法
i -= 1
j -= 1
# 检查 135 度角是否有皇后
i, j = row - 1, col + 1
while i >= 0 and j < len(chessboard):
if chessboard[i][j] == 'Q':
return False # 右上方向已经存在皇后,不合法
i -= 1
j += 1
return True # 当前位置合法
解数独
相关技巧:解数独的题目我认为应该是非常难的了。我们先来理清理清我们的思路,我们就是再空白的地方填上数字,但是其填的数字是否合法呢?是有要求的,三个要求:**同行不能出现过,同列不能出现过,同个九宫格里面不能出现过。**当三个要求都满足的时候,我们就可以放入该数字了。
其实这么想跟N皇后问题是差不多的,但是其还是要比N皇后难很多的,N皇后一行放一个之后,就可也换下一行了,数独必须将一行放满才可以换下一行,具体看看下图理解下。
- 确定回溯的参数和返回值:递归函数的返回值需要是bool类型,为什么呢?因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
- 确定回溯的终止条件:本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。不用终止条件会不会死循环?递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
- 确定单层回溯的逻辑:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!注意这里return false的地方,这里放return false 是有讲究的。因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
row_used = [set() for _ in range(9)]
col_used = [set() for _ in range(9)]
box_used = [set() for _ in range(9)]
for row in range(9):
for col in range(9):
num = board[row][col]
if num == ".":
continue
row_used[row].add(num)
col_used[col].add(num)
box_used[(row // 3) * 3 + col // 3].add(num)
self.backtracking(0, 0, board, row_used, col_used, box_used)
def backtracking(
self,
row: int,
col: int,
board: List[List[str]],
row_used: List[List[int]],
col_used: List[List[int]],
box_used: List[List[int]],
) -> bool:
if row == 9:
return True
next_row, next_col = (row, col + 1) if col < 8 else (row + 1, 0)
if board[row][col] != ".":
return self.backtracking(
next_row, next_col, board, row_used, col_used, box_used
)
for num in map(str, range(1, 10)):
if (
num not in row_used[row]
and num not in col_used[col]
and num not in box_used[(row // 3) * 3 + col // 3]
):
board[row][col] = num
row_used[row].add(num)
col_used[col].add(num)
box_used[(row // 3) * 3 + col // 3].add(num)
if self.backtracking(
next_row, next_col, board, row_used, col_used, box_used
):
return True
board[row][col] = "."
row_used[row].remove(num)
col_used[col].remove(num)
box_used[(row // 3) * 3 + col // 3].remove(num)
return False
其他
递增子序列
相关技巧:如果你能够理解解数独的方法,再来做这些回溯的题目就是轻轻松松了。这题目就跟子集II的很像的,我们是通过排序,再加一个标记数组来达到去重的目的。而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!,也就是说,我们不能再用之前的used数组来去重了。
看图,我们以[4,7,4,7]为例。
- 确定回溯的参数和返回值:首先的肯定是nums数组,然后很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。然后就是path来存储我们的路径,用来加入新的和弹出操作,最后当结果符合,我们肯定需要一个result来保存我们的结果。至于返回值就不需要了,因为我们有result参数来保存我们的结果了。
- 确定回溯的终止条件:我们不需要加终止条件,因为我们需要长度大于2的中间的节点,那会不会无限递归回溯下去呢?当然不会,因为有startIndex参数,超过长度了自然就终止了。
- 确定单层回溯的逻辑:注意看我们再每层的时候都会有个uset用来去重,就是再同一父节点下的本层,值相同的节点不能够重复使用,看图就能很好的理解了。
class Solution:
def findSubsequences(self, nums):
result = []
path = []
self.backtracking(nums, 0, path, result)
return result
def backtracking(self, nums, startIndex, path, result):
if len(path) > 1:
result.append(path[:]) # 注意要使用切片将当前路径的副本加入结果集
# 注意这里不要加return,要取树上的节点
uset = set() # 使用集合对本层元素进行去重
for i in range(startIndex, len(nums)):
if (path and nums[i] < path[-1]) or nums[i] in uset:
continue
uset.add(nums[i]) # 记录这个元素在本层用过了,本层后面不能再用了
path.append(nums[i])
self.backtracking(nums, i + 1, path, result)
path.pop()
重新安排行程
相关技巧:这题乍一看哈,感觉不像是用回溯做的题目,倒像是DFS深度搜索。其实哈,本质上就是DFS深度搜索,但是其中用到了回溯的思想,我们具体看看怎么做的。
首先我觉得最大的难点应该就是字母序靠前排前面,我们怎么记录这个映射关系呢?其实哈这个在python中还是挺容易解决的,我们直接用**tickets.sort(key=lambda x:x[1])**这样可以根据航班每一站的重点字母顺序排序。
- 确定回溯的参数和返回值:我们需要airport来记录当前的机场,同时要一个targets参数来记录我们当前的机场能够到达的下个机场都有哪些。最后就是result来记录我们的结果就行了。
- 确定回溯的终止条件:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。
- 确定单层回溯的逻辑:当当前还有可到达机场的时候,弹出机场继续递归回溯的过程,最后的时候我们倒着顺序加入我们在最终结果,输出的时候记得倒序输出哦。
from collections import defaultdict
class Solution:
def findItinerary(self, tickets):
targets = defaultdict(list) # 创建默认字典,用于存储机场映射关系
for ticket in tickets:
targets[ticket[0]].append(ticket[1]) # 将机票输入到字典中
for key in targets:
targets[key].sort(reverse=True) # 对到达机场列表进行字母逆序排序
result = []
self.backtracking("JFK", targets, result) # 调用回溯函数开始搜索路径
return result[::-1] # 返回逆序的行程路径
def backtracking(self, airport, targets, result):
while targets[airport]: # 当机场还有可到达的机场时
next_airport = targets[airport].pop() # 弹出下一个机场
self.backtracking(next_airport, targets, result) # 递归调用回溯函数进行深度优先搜索
result.append(airport) # 将当前机场添加到行程路径中