回溯篇 - 棋盘问题 图的dfs
题目链接
这道题的思路是什么样的,也就是递归树是怎么画的
其实是,棋盘的宽度,就是每层递归的遍历宽度,我们在每层递归逻辑中,就是一列列去试着放,如果不合法(就是同行同列对角线),我们就continue, 如果合法了,和之前几行放的都不冲突,我们递归下一行
递归的深度其实就是我们棋盘的高度,我们是一行一行的从上往下去递归,所以递归的出口也就是 我们的行数参数 row == n了。 为什么是等于n,不是n-1呢,因为我们需要把n-1行的操作完成了,我们再把所有的棋盘内容塞进res
这道题我再把递归三部曲写一下吧:
1.递归函数参数,我们需要 chessboard, row, n
因为我们需要把棋盘传进来,也需要row控制深度也就是出口,n来给每层遍历的宽度
2.出口
出口我上面分析了,row == n,表示0 - (n-1)行都合法并递归填Q完成了,我们把这种方案给加到res里
3.每层递归的逻辑
其实就是遍历每一列,一列一列的去试,如果说,合法了,好我们就把Q填到这一列,然后递归下一行;
isvalid函数的构造需要chessbord, row, col 就是此时的键盘矩阵 和 此时的坐标
代码写在下面了
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
chessboard = [['.'] * n for _ in range(n)]
res = []
def isvalid(chessboard, row, col):
# 1.皇后不能在同一行 (不用特判,因为我们一行就填一个Q就递归进入下一行了)
# 2.皇后不能在同一列, 由于我们的判别在填Q之前,所以我们遍历每行的这一列,看有无Q
for i in range(n):
if chessboard[i][col] == 'Q':
return False
#3.特判45°角,也就是左上方是否有Q, 因为左上角递归填过了
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if chessboard[i][j] == 'Q':
return False
i -= 1
j -= 1
#4.特判135°角,右上角是否有Q,因为右上角递归填过
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 rec(chessboard, row, n):
# 写出口,看要求,力扣要求是要把chessboard转成每一行字符串,如果不要求就不用
if row == n: #出口
temp_res = []
for i in chessboard:
temp_str = ''.join(i)
temp_res.append(temp_str)
res.append(temp_res)
for col in range(n):
if not isvalid(chessboard,row,col): continue
chessboard[row][col] = 'Q'
rec(chessboard,row+1,n)
chessboard[row][col] = '.'
rec(chessboard,0,n)
return res
题目链接
N皇后问题,是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。
解数独就不一样了,本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
本题是一种二维的递归,所以和之前的操作有些不一样,特别是递归函数的返回值等这些细节需要好好琢磨一下,因此我把递归三部曲写一下:
1.递归函数以及参数
递归函数的返回值应该是bool类型的
因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,
2. 递归出口
本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。
不用终止条件会不会死循环?
递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
那么有没有永远填不满的情况呢?
这个问题我在递归单层搜索逻辑里在来讲!
3.单层递归的搜索逻辑
在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
每一层的遍历就是在一个空格位置上去试1-9 九个数,如果可以就填上,然后反正下一层递归会直接找到下一个空格再1-9去试
def rec(board):
for i in range(9):
for j in range(9):
if board[i][j] != '.':
continue
for k in range(1,10): #对于每一个空格 用1-9去填
if isValid(board,i,j,k):
board[i][j] = str(k)
if rec(board): return True #解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回 #节点到叶子节点一条唯一路径,所以需要使用bool返回值
board[i][j] = '.'
return False #如果1-9填都不行,返回False
return True
注意这里return false的地方,这里放return false 是有讲究的。
因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去。
所以说,递归函数是bool返回值,以及没有出口会不会一直无限递归在这里填上了
判断棋盘是否合法:
同行 同列 九宫格是否重复
def isValid(board,row,col,k):
#每一行不能重复
for i in range(9):
if board[row][i] == str(k):
return False
#每一列不能重复
for i in range(9):
if board[i][col] == str(k):
return False
#每一个九宫格不能重复
start_i = (row//3) * 3
start_j = (col//3) * 3
for i in range(start_i,start_i+3):
for j in range(start_j,start_j+3): #遍历某个空格所在的九宫格
if board[i][j] == str(k):
return False
return True
整体代码如下:
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
def isValid(board, row, col, k):
#每一行不能重复
for j in range(9):
if board[row][j] == str(k):
return False
#每一列不能重复
for i in range(9):
if board[i][col] == str(k):
return False
#每一小块九宫格里的数字不能重复
start_i, start_j = (row // 3)*3, (col // 3)*3 #satrt_i, start_j是把普通(row,col)坐标转换到它所在的小九宫格的[0][0]位置
for i in range(start_i, start_i+3):
for j in range(start_j, start_j+3):
if board[i][j] == str(k):
return False
return True
def rec(board):
#不用写出口,返回值是bool值
#直接写每层递归搜索逻辑
# 每层都从第一个数开始,遍历行列,找到第一个空格的位置,然后1-9一个个去试
for i in range(9):
for j in range(9):
if board[i][j] != '.':
continue
for k in range(1, 10):
if isValid(board, i, j, k):
board[i][j] = str(k)
if rec(board): return True #也许这就是出口的一种吧,没有遍历完棋盘没有空了,都填满了,说明找到合适一组立刻返回
board[i][j] = '.'
return False #某个位置1-9试了个遍,说明这个无解,返回False
return True
rec(board)
题目链接
defaultdict(list)的用法
所有的机票 必须都用一次 且 只能用一次,因此不能重复飞
递归三部曲:
1.递归函数以及参数
函数的返回值应该是bool, 只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图, 找到了这个叶子节点了直接返回, 参数 我就有一个start,表示每层递归开始的地方,一开始从’JFT’,每层都更新
2.递归的出口
什么时候找到一条路呢?就是三张票,我肯定有4个机场吧,如果我们的path里收集的机场数是ticket+1了我们就return True
3.单层递归的逻辑
还是根据图示的递归树去实现,这一层的开始结点,是上一层的fly to 的结点(字典序第一个)
dict[start].sort() #字典序
for i in dict[start]:
end = dict[start].pop(0) #递归的下一个开始点
path.append(end)
if rec(end):# 只要找到一个就可以返回了
return True
path.pop()
dict[start].append(end)#递归的下一个开始点
整体代码:
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
dict = defaultdict(list)
for item in tickets:
dict[item[0]].append(item[1])
path = ['JFK']
def rec(start):
if len(path) == len(tickets)+1: #出口
return True
dict[start].sort() #将start结点能飞到的机场做字典序排序
for i in dict[start]:
end = dict[start].pop(0) #递归的下一个开始点
path.append(end)
if rec(end):# 只要找到一个就可以返回了
return True
path.pop()
dict[start].append(end)#递归的下一个开始点
rec('JFK')
return path
在代码随想录里学习了回溯算法能解决的这几种问题:
组合问题和切割问题收集的是叶子结点, 子集问题收集的是所有结点,这两种的每层递归逻辑里都需要一个start, 下层从上层start的下一个开始
全排列问题不需要start, 它是下一层也从头开始遍历。所以涉及了很多去重问题
如果说nums本身有很多重复,那么我们需要在每一层的遍历时去重, 可以排序之后去重也可以用一个set去重
如果说像子集问题需要在每个枝条上去重的话,有简单的if nums[i] in path,但是如果每层去重和枝条上去重混合的时候就不能用这个了,因为可能每一层就相同的数字,必须用used数组去回溯,标记每一位上一层是否用过,上一层用过的位置标1,下一层不能继续用
以上是我刷完回溯篇的简单体会
代码随想录回溯篇所有题的递归树分析就在这一篇里14道这里的总结比我更细致