原文请看回溯算法框架
这两天在刷Leetcode N皇后和单词搜索时,探究回溯算法的解题思路,结合资料整理框架模版,便于总结和参考。解决回溯问题,就是解决决策树问题,当前的决策对后面的选择至关重要,话说条条大路通罗马,每一次决策过程就是遍历一条可走得通的路,但是算法是有条件的,往往只有一条几条路走得通。
在决策过程中过程中,要考虑三个问题:
- 路径选择: 已经做出的选择
- 选择列表: 所有可供的选择
- 结束条件: 也就是决策树底层,无法再做出其他选择
代码模版则是:
result = []
def backtrack(路径, 选择列表):
if 满足条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
暂时不理解没关系,我也是在做题时把模版往题上套才慢慢明白。
LeetCode 79 单词搜索
题目描述:
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
分析
本题从网格中相邻的字符串中找出word,题目要求同一个单元格的字母不允许重复,如上图示例1,找到第一个’C’ 时,即可以继续向右边寻找,结果是‘E’,不对,又可以返回向下寻找’C‘,对了,继续在第二个’C’的相邻单元格中寻找,就是上述模版:
做选择-〉判断是否满足要求-〉撤销选择的过程
代码模版可以大致为:
result = []
def backtrack(word, board):
# 满足 return False
if board[i][j] != word[k]:
return 失败
if 找到 word:
return 成功
# 网格中遍历
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
代码解析
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
self.board = board
self.word = word
if self.board == None:
return False
self.h = len(self.board)
self.w = len(self.board[0])
self.help_list = [[0] * len(self.board[0]) for _ in range(len(self.board))]
for i in range(len(board)):
for j in range(len(board[0])):
if self.backtrack(i, j, 0):
return True
print(self.help_list)
return False
def backtrack(self, i, j, k):
if self.board[i][j] != self.word[k]:
return False
self.help_list[i][j] = 1
if k == len(self.word) - 1:
return True
# 方法一: 先设定方向,利用数组选择方向
# direction = [(0,1), (0,-1), (-1,0), (1,0)]
# for di, dj in direction:
# newdi, newdj = i + di, j + dj
# if 0 <= newdi < len(self.board) and 0 <= newdj < len(self.board[0]):
# if self.help_list[newdi][newdj] == 0:
# if self.backtrack(newdi, newdj, k + 1):
# result = True
# break
# 方法二: 计算方向
# 回溯法原理,只要有其中一个结果符合就进行下一步操作, 不符合这试试相邻的, 结果之间不互斥,
# 但是所有结果都不符合则撤销该选择,返回False
if 0 <= j+1 < self.w and self.help_list[i][j+1] == 0:
if self.backtrack(i, j+1, k+1):
return True
if self.w > j-1 >= 0 and self.help_list[i][j-1] == 0:
if self.backtrack(i, j-1, k+1):
return True
if self.h > i-1 >= 0 and self.help_list[i-1][j] == 0:
if self.backtrack(i-1, j, k+1):
return True
if 0 <= i+1 < self.h and self.help_list[i+1][j] == 0:
if self.backtrack(i+1, j, k+1):
return True
self.help_list[i][j] = 0
return False
总结
上述代码中方法一代码更简洁,方向上利用数组复制,避免了方法二的代码冗余。
- 在遇到类似
方向
相关的题是用数组改变方向灵活性强。 - 回溯问题每一个子决策之间不互斥,只要有一个走通就可以返回True
- 决策不成功则
还原现场
LeetCode 51 N皇后
题目描述:
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
分析
皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。假定斜线分别为撇和捺,位于撇线
上的坐标相加等于一个固定值,位于捺线
上的坐标相减等于一个固定值。如下图所示,该皇后的辐射范围为:
下一个皇后在放置前,先判断是否在其他皇后攻击范围之内。
路径:board 中⼩于当前行row的那些⾏都已经成功放置了皇后
选择列表:当前行row的所有列都是放置皇后的选择
结束条件:row 超过 board 的最后⼀⾏
代码解析
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
if n < 1: return []
self.n = n
# 方法一:
# self.result = []
# self.col = set()
# self.pei = set()
# self.na = set()
# self.dfs1(n, 0, [])
# return self._generate_result(n)
# 方法二:
self.result = []
self._dfs2([], [], [])
return [["." * i + "Q" + "." * (n - 1 - i) for i in col] for col in self.result]
# 方法一:
def dfs1(self, n, row, current_col):
if row >= n:
self.result.append(current_col)
return
for col in range(n):
# 寻找合适位置
if col in self.col or col + row in self.pei or row - col in self.na:
continue
# 加入条件
self.col.add(col)
self.pei.add(row + col)
self.na.add(row - col)
# 寻找下一行合适的位置,current_col 存放每一次合适的列元素索引
self.dfs(n, row + 1, current_col + [col])
# 如果下一层执行到col = n 则返回到此步,没有合适位置,执行后面一列
self.col.remove(col)
self.pei.remove(row + col)
self.na.remove(row - col)
def _generate_result(self, n):
b = []
for res in self.result:
for i in res:
b.append("." * i + "Q" + "." * (n - 1 - i))
return [b[i: i + n] for i in range(0, len(b), n)]
# 方法二: xy_diff=pei, xy_sum = na
def _dfs2(self, queue, xy_diff, xy_sum):
# 行,每一次从这里更新,如果上一个操作满足, 则行 + 1
p = len(queue)
if p == self.n:
self.result.append(queue)
# 列, queue
for q in range(self.n):
# queue 存放q(列)的值
# 此时p = len(queue) 已经到下一行,从下一行的第一列开始计算 p-q 和 p+q.
if q not in queue and p - q not in xy_diff and p + q not in xy_sum:
self._dfs2(queue + [q], xy_diff + [p - q], xy_sum + [p + q])
总结
方法二功底很深厚,学习学习
祝好!
更多文章,请关注公众号:编程牧马人