目录
回溯法求解迷宫问题
什么是迷宫问题
假设主体(人、动物或者飞行器)放在一个迷宫地图入口处,迷宫中有许多墙,使得大多数的路径都被挡住而无法行进。主体可以通过遍历所有可能到出口的路径来到达出口。当主体走错路时需要将走错的路径记录下来,避免下次走重复的路径,直到找到出口。
回溯求解迷宫问题
回溯法求解迷宫问题一般思路是枚举所有情况,当发现某个解的方向不准确时就不往下进行,而是回溯到上一层,这个过程实现了减枝,也就是“下一步走不通再回头”。特点是在搜索过程中寻找问题的解,一旦发现不满足条件便回溯,继续搜索其他路径,提高效率。
问题描述与求解
在《程序员面试金典》中有这样一道题
迷路的机器人
https://leetcode.cn/problems/robot-in-a-grid-lcci/description/?q=动态规划&orderBy=most_relevant
设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。
网格中的障碍物和空位置分别用 1
和 0
来表示。
返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。
示例 1:
输入: [
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: [[0,0],[0,1],[0,2],[1,2],[2,2]]
解释: 输入中标粗的位置即为输出表示的路径,即
0行0列(左上角) -> 0行1列 -> 0行2列 -> 1行2列 -> 2行2列(右下角)
解题思路
机器人位置转移
对于该问题,当我们知道机器人位于坐标 ( x , y ) (x,y) (x,y)时,机器人只能往右和往下两个方向移动,也就是 ( x + 1 , y ) (x+1,y) (x+1,y)和 ( x , y + 1 ) (x,y+1) (x,y+1),之后不停地往两个方向运动直到到达终点
回溯减枝
-
如果走到坐标 ( x + 1 , y ) (x+1,y) (x+1,y)有障碍物时,说明下一步不可行,则退回上一步,再换另一个方向到 ( x , y + 1 ) (x,y+1) (x,y+1)去寻找可达到右下角的路径
-
用
visited
数组记录已访问过的节点,并且该节点退出回溯过程时不用还原(答案用obstacleGrid[x][y] = 1
实现该过程)
无后效性
一般回溯时退出回溯过程的节点visited
数组要还原到未遍历的状态,而如果这样做会导致超时,在该题中不用还原会提高运行速度(减枝)并且不影响正确答案。这里就有一个问题:为什么不还原visited
会好一点?其实牵涉到了动态规划的一个基本原理:无后效性。
对于无后有效性,就是T
时间到达了某个状态,在对后面要到达什么的状态进行决策的时候,和T
时间之前的决策没有关系,也可以说,T
时间之前的决策,对T
时间之后的决策,不会产生任何影响。
在本题的体现就是:
不管我之前是怎么到达 ( x , y ) (x,y) (x,y)这个坐标的,我在选择要走到别的格子的时候,只能向下或向右,和之前怎么走到 ( x , y ) (x,y) (x,y)是无关的。那么可以想象:如果我曾经经过 ( x , y ) (x,y) (x,y)这个坐标,并且继续往下走得很深(分支都搜完了),但是最终失败了,那么当我再次从别的格子走到 ( x , y ) (x,y) (x,y)时,也就没有必要继续往下搜了,因为它接下来还是只能往下或往右走,并且最终一定会失败。
个人理解就是在当前坐标之后的运动目标不影响之前的运动轨迹(不会回头走
程序细节
-
走不通就对
path
数组的节点pop
,但是标记还是保留 -
flag
作为答案标记,找到答案就停止对path
数组pop
操作 -
注意边界和障碍物要退出回溯
class Solution(object):
def pathWithObstacles(self, obstacleGrid):
"""
:type obstacleGrid: List[List[int]]
:rtype: List[List[int]]
"""
m = len(obstacleGrid)
n = len(obstacleGrid[0])
path = []
self.flag = False
def search(x,y):
if x == m or y == n or self.flag:
return
if obstacleGrid[x][y] == 1:
return
obstacleGrid[x][y] = 1
path.append([x,y])
if x == m - 1 and y == n - 1:
self.flag = True
return
search(x , y + 1)
search(x + 1 , y)
if not self.flag:path.pop()#找到路径后回退时不执行pop()保留路径
search(0,0)
return path
dp访问标记解法
https://leetcode.cn/problems/robot-in-a-grid-lcci/solutions/1514882/wei-rao-li-lun-by-wfnuser-2rl2/
转移方程
对于动态规划,要考虑状态转移。对于坐标
(
x
,
y
)
(x,y)
(x,y)而言,状态从
(
x
−
1
,
y
)
(x-1,y)
(x−1,y)和
(
x
,
y
−
1
)
(x,y-1)
(x,y−1)获取。即坐标
(
x
,
y
)
(x,y)
(x,y)的状态dp[x][y]
取决于dp[x-1][y]
和dp[x][y-1]
。只要坐标
(
x
−
1
,
y
)
(x-1,y)
(x−1,y)和
(
x
,
y
−
1
)
(x,y-1)
(x,y−1)有一个可达,坐标
(
x
,
y
)
(x,y)
(x,y)也是可达的,与之前的路径转移过程无关。为了求解路径,只需要在每个坐标
(
x
,
y
)
(x,y)
(x,y)下记录转移到该坐标的上一个坐标即可。
初始化
初始化dp
元素为-1
,表示当前位置不可达。dp[0][0] = 0
转移实现
如果dp[x][y] ≥ 0
则表示该坐标可达,具有转移条件,这里的坐标进行了编码处理,即(x,y) → x*row + y
可以节省内存,如果dp[-1][-1] ≠ -1
说明有路径,最后求答案需要对坐标解码
class Solution(object):
def pathWithObstacles(self, obstacleGrid):
"""
:type obstacleGrid: List[List[int]]
:rtype: List[List[int]]
"""
m = len(obstacleGrid)
n = len(obstacleGrid[0])
result = []
if obstacleGrid[0][0] == 1:return []
dp = [[-1]*n for i in range(m)]
if obstacleGrid[0][0]:return result
dp[0][0] = 0
for x in range(m):
for y in range(n):
if obstacleGrid[x][y] or (x == 0 and y == 0):
continue
if dp[x-1][y] >= 0:
dp[x][y] = (x-1)*n + y
if dp[x][y-1] >= 0:
dp[x][y] = x*n + y - 1
if dp[-1][-1] == -1:
return result
else:
x , y = m - 1 , n - 1
while [x , y] != [0 , 0]:
result.append([x,y])
tmp = dp[x][y]
x = tmp // n
y = tmp % n
result.append([x,y])
return result[::-1]
dp的另一种实现方式
对坐标不进行编码,使用list存储,并通过dict实现dp状态转移
class Solution(object):
def pathWithObstacles(self, obstacleGrid):
"""
:type obstacleGrid: List[List[int]]
:rtype: List[List[int]]
"""
m = len(obstacleGrid)
n = len(obstacleGrid[0])
result = []
if obstacleGrid[0][0] == 1:return []
dp = collections.defaultdict(list)
if obstacleGrid[0][0]:return result
dp[0,0] = [0, 0]
for x in range(m):
for y in range(n):
if obstacleGrid[x][y] or (x == 0 and y == 0):
continue
if dp[x-1 , y]:
dp[x , y] = [x - 1 , y]
if dp[x , y-1]:
dp[x , y] = [x , y - 1]
if dp[m - 1 , n - 1]:
cur = [m - 1 , n - 1]
while cur != [0,0]:
result.append(cur)
cur = dp[tuple(cur)]
result.append(cur)
return result[::-1]
else:
return result
迷宫问题拓展问题
剑指Offer12 矩阵中的路径
https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/description/
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
不允许被重复使用体现了后有效性
参考解答
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
rows = len(board)
cols = len(board[0])
self.l = len(word)
visited = [[False]*cols for _ in range(rows)]
self.flag = False
def backtracking(row,col,wordnum):
if visited[row][col] == False:
if wordnum == self.l-1:
self.flag = True
return
wordnum += 1 #满足界内判断下一个字符
visited[row][col] = True
if row < rows-1 and board[row+1][col] == word[wordnum]:backtracking(row+1, col, wordnum)
if row > 0 and board[row-1][col] == word[wordnum]:backtracking(row-1, col, wordnum)
if col < cols-1 and board[row][col+1] == word[wordnum]:backtracking(row, col+1, wordnum)
if col > 0 and board[row][col-1] == word[wordnum]:backtracking(row, col-1, wordnum)
visited[row][col] = False
return
for i in range(rows):
for j in range(cols):
if board[i][j] == word[0]:
backtracking(i, j, 0)
return self.flag