本文介绍 LeetCode 题集中,使用回溯法(递归)解决矩阵相关的问题。
LeetCode 其他有关回溯法的问题:
LeetCode 题集:回溯法和递归(一)数组相关问题
LeetCode 题集:回溯法和递归(二)字符串相关问题
79. Word Search(单词搜索)
问题描述
思路与代码
本题的思路,遍历矩阵所有元素,以其为起始点,向各个方向的相邻点搜索,通过回溯法进行求解,只要发现符合的连通域,即返回 True。
代码如下:
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def backtrack(cor: Tuple[int, int], pos: int, mat_used: List[List[bool]]) -> bool:
"""
回溯法递归函数
:param cor: 当前坐标
:param pos: 单词搜索的当前字母位置
:param mat_used: 矩阵元素是否被使用
:return: 单词是否找到
"""
if board[cor[0]][cor[1]] == word[pos]:
if pos == len(word) - 1:
return True
else:
mat_used[cor[0]][cor[1]] = True
if cor[0] > 0 and not mat_used[cor[0] - 1][cor[1]]: # 向左
if backtrack(cor=(cor[0] - 1, cor[1]), pos=pos + 1, mat_used=mat_used):
return True
else:
mat_used[cor[0] - 1][cor[1]] = False
if cor[0] < len(board) - 1 and not mat_used[cor[0] + 1][cor[1]]: # 向右
if backtrack(cor=(cor[0] + 1, cor[1]), pos=pos + 1, mat_used=mat_used):
return True
else:
mat_used[cor[0] + 1][cor[1]] = False
if cor[1] > 0 and not mat_used[cor[0]][cor[1] - 1]: # 向上
if backtrack(cor=(cor[0], cor[1] - 1), pos=pos + 1, mat_used=mat_used):
return True
else:
mat_used[cor[0]][cor[1] - 1] = False
if cor[1] < len(board[0]) - 1 and not mat_used[cor[0]][cor[1] + 1]: # 向下
if backtrack(cor=(cor[0], cor[1] + 1), pos=pos + 1, mat_used=mat_used):
return True
else:
mat_used[cor[0]][cor[1] + 1] = False
else:
return False
# 起始点
for i in range(len(board)):
for j in range(len(board[0])):
mat_used_ = [[False for _ in range(len(board[0]))] for _ in range(len(board))]
if backtrack(cor=(i, j), pos=0, mat_used=mat_used_):
return True
return False
运行效果:
54. Spiral Matrix(螺旋矩阵)
问题描述
思路与代码
本题的思路称不上回溯法,但与前一题的搜索方法相似,且更为简单,可通过一次递归完成遍历。
代码如下:
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not len(matrix):
return []
def rec(cor: Tuple[int, int], direction: str, num_rem: int,
mat_visit: List[List[bool]], list_visit: List[int]) -> List[int]:
"""
递归函数
:param cor: 当前位置
:param direction: 当前搜索方向
:param num_rem: 剩余点数
:param mat_visit: 矩阵元素是否已被访问
:param list_visit: 已访问的点列表
:return: list_visit: 已访问的点列表(更新)
"""
if not num_rem:
return list_visit
# 调整搜索方向
if direction == 'right':
if not (cor[1] < len(matrix[0]) - 1 and not mat_visit[cor[0]][cor[1] + 1]): # 右转下
direction = 'down'
if direction == 'down':
if not (cor[0] < len(matrix) - 1 and not mat_visit[cor[0] + 1][cor[1]]): # 下转左
direction = 'left'
if direction == 'left':
if not (cor[1] > 0 and not mat_visit[cor[0]][cor[1] - 1]): # 左转上
direction = 'up'
if direction == 'up':
if not (cor[0] > 0 and not mat_visit[cor[0] - 1][cor[1]]): # 上转右
direction = 'right'
# 继续遍历
if direction == 'right':
list_visit.append(matrix[cor[0]][cor[1] + 1])
mat_visit[cor[0]][cor[1] + 1] = True
list_visit = rec(cor=(cor[0], cor[1] + 1), direction=direction, num_rem=num_rem - 1,
mat_visit=mat_visit, list_visit=list_visit)
elif direction == 'down':
list_visit.append(matrix[cor[0] + 1][cor[1]])
mat_visit[cor[0] + 1][cor[1]] = True
list_visit = rec(cor=(cor[0] + 1, cor[1]), direction=direction, num_rem=num_rem - 1,
mat_visit=mat_visit, list_visit=list_visit)
elif direction == 'left':
list_visit.append(matrix[cor[0]][cor[1] - 1])
mat_visit[cor[0]][cor[1] - 1] = True
list_visit = rec(cor=(cor[0], cor[1] - 1), direction=direction, num_rem=num_rem - 1,
mat_visit=mat_visit, list_visit=list_visit)
else:
list_visit.append(matrix[cor[0] - 1][cor[1]])
mat_visit[cor[0] - 1][cor[1]] = True
list_visit = rec(cor=(cor[0] - 1, cor[1]), direction=direction, num_rem=num_rem - 1,
mat_visit=mat_visit, list_visit=list_visit)
return list_visit
mat_visit_ = [[False for _ in range(len(matrix[0]))] for _ in range(len(matrix))]
mat_visit_[0][0] = True
list_visit_ = [matrix[0][0]]
list_visit_ = rec(cor=(0, 0), direction='right', num_rem=len(matrix) * len(matrix[0]) - 1,
mat_visit=mat_visit_, list_visit=list_visit_)
return list_visit_
运行效果:
59. Spiral Matrix II(螺旋矩阵 II)
问题描述
思路与代码
本题实际上并不需要用到回溯法或递归函数,但由于是前一题的变体,因此顺便列出。
代码如下:
class Solution:
def generateMatrix(self, n: int) -> List[List[int]]:
if not n:
return [[]]
matrix = [[0 for _ in range(n)] for _ in range(n)]
num_rem = n ** 2 # 剩余数量
i, j = 0, 0
matrix[i][j] = n ** 2 - num_rem + 1
num_rem -= 1
direction = 'right'
while num_rem:
# 更新行进方向
if direction == 'right':
if not (j < n - 1 and not matrix[i][j + 1]):
direction = 'down'
if direction == 'down':
if not (i < n - 1 and not matrix[i + 1][j]):
direction = 'left'
if direction == 'left':
if not (j and not matrix[i][j - 1]):
direction = 'up'
if direction == 'up':
if not (i and not matrix[i - 1][j]):
direction = 'right'
# 遍历填数
if direction == 'right':
j += 1
matrix[i][j] = n ** 2 - num_rem + 1
elif direction == 'down':
i += 1
matrix[i][j] = n ** 2 - num_rem + 1
elif direction == 'left':
j -= 1
matrix[i][j] = n ** 2 - num_rem + 1
else:
i -= 1
matrix[i][j] = n ** 2 - num_rem + 1
num_rem -= 1
return matrix
运行效果:
51. N-Queens(N 皇后)
问题描述
思路与代码
本题思路为经典的回溯法,笔者在之前的博客中曾有所介绍:
具体代码如下:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
list_result = []
list_state = [-1 for _ in range(n)]
def is_valid(row: int, column: int) -> bool:
"""
judge valid for a cell
:param row: row index of current cell
:param column: column index of current cell
:return: if current cell is valid
"""
for i in range(row):
# next row: avoid covered columns and diagonals
if (list_state[i] == column) or (abs(row - i) == abs(column - list_state[i])):
return False
return True
def backtrack(row: int):
"""
backtracking, recursion
:param row: current row index
:return: nothing
"""
# end condition
if row == n:
list_solution = []
for i in range(n):
str_row = ''
for j in range(n):
str_row += 'Q' if j == list_state[i] else '.'
list_solution.append(str_row)
list_result.append(list_solution.copy())
return
# recursion body
for column in range(n):
if is_valid(row=row, column=column):
list_state[row] = column
backtrack(row=row + 1)
list_state[row] = -1
backtrack(row=0)
return list_result
运行效果:
实际上,回溯法还可以通过非递归的方式(while 循环)实现,具体代码如下:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
list_result = []
list_state = [-1 for _ in range(n)]
def is_valid(x, y):
for i in range(x):
if list_state[i] == y or abs(x - i) == abs(y - list_state[i]):
return False
return True
row = 0
while True:
column = list_state[row] + 1
while column < n:
if is_valid(x=row, y=column):
list_state[row] = column
# a new solution found
if row == n - 1:
solution = [''.join(['Q' if j == list_state[i] else '.' for j in range(n)]) for i in range(n)]
list_result.append(solution)
# a valid state, go on for the next queen
else:
row += 1
break
column += 1
if column == n:
if not row: # all solutions found, search end
break
list_state[row] = -1
row -= 1 # back to last row to get other solutions
return list_result
运行效果:
上述方法为标准的回溯法,查看官方题解,发现一种基于位运算的回溯法更加巧妙,具体思路如下:
代码如下:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
def generate_board():
board = list()
for i in range(n):
row[queens[i]] = 'Q'
board.append(''.join(row))
row[queens[i]] = '.'
return board
def solve(row: int, columns: int, diagonals_1: int, diagonals_2: int):
if row == n:
board = generate_board()
solutions.append(board)
else:
available_positions = ((1 << n) - 1) & (~(columns | diagonals_1 | diagonals_2))
while available_positions:
position = available_positions & (-available_positions) # 获得二进制表示中的最低位的 1 的位置
available_positions = available_positions & (available_positions - 1) # 将二进制表示中的最低位的 1 置成 0
# 存储当前可行列
column = bin(position - 1).count('1')
queens[row] = column
# 下一行
solve(row=row + 1, columns=columns | position, diagonals_1=(diagonals_1 | position) << 1, diagonals_2=(diagonals_2 | position) >> 1)
solutions = list()
queens = [-1] * n
row = ["."] * n
solve(row=0, columns=0, diagonals_1=0, diagonals_2=0)
return solutions
运行效果:
此外,从运筹学的角度讲,还有一种约束规划的方法,使用 OR-Tools CP-SAT 求解器获取所有可行解,具体操作方法可见笔者之前的博客(前文有链接)。
52. N-Queens II(N皇后 II)
问题描述
思路与代码
本题为前一题的变体,由于只需返回可行解的数量,代码中可省去很多结果的存储,只需计数即可。
笔者在此选用基于位运算的回溯法,代码如下:
class Solution:
def totalNQueens(self, n: int) -> int:
self.num_solution = 0
def solve(row: int, columns: int, diagonals_1: int, diagonals_2: int):
if row == n:
self.num_solution += 1
return
else:
available_positions = ((1 << n) - 1) & (~(columns | diagonals_1 | diagonals_2))
while available_positions:
position = available_positions & (-available_positions) # 获得二进制表示中的最低位的 1 的位置
available_positions = available_positions & (available_positions - 1) # 将二进制表示中的最低位的 1 置成 0
solve(row=row + 1, columns=columns | position,
diagonals_1=(diagonals_1 | position) << 1, diagonals_2=(diagonals_2 | position) >> 1) # 下一行
solve(row=0, columns=0, diagonals_1=0, diagonals_2=0)
return self.num_solution
运行效果: