回溯法求解迷宫问题

目录

回溯法求解迷宫问题

什么是迷宫问题

假设主体(人、动物或者飞行器)放在一个迷宫地图入口处,迷宫中有许多墙,使得大多数的路径都被挡住而无法行进。主体可以通过遍历所有可能到出口的路径来到达出口。当主体走错路时需要将走错的路径记录下来,避免下次走重复的路径,直到找到出口。

回溯求解迷宫问题

回溯法求解迷宫问题一般思路是枚举所有情况,当发现某个解的方向不准确时就不往下进行,而是回溯到上一层,这个过程实现了减枝,也就是“下一步走不通再回头”。特点是在搜索过程中寻找问题的解,一旦发现不满足条件便回溯,继续搜索其他路径,提高效率。

问题描述与求解

在《程序员面试金典》中有这样一道题

迷路的机器人

https://leetcode.cn/problems/robot-in-a-grid-lcci/description/?q=动态规划&orderBy=most_relevant

设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。

在这里插入图片描述

网格中的障碍物和空位置分别用 10 来表示。

返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 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实现该过程)

无后效性

https://leetcode.cn/problems/robot-in-a-grid-lcci/solutions/1112244/cong-wu-hou-xiao-xing-yuan-lai-nong-dong-spqj/

一般回溯时退出回溯过程的节点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) (x1,y) ( x , y − 1 ) (x,y-1) (x,y1)获取。即坐标 ( x , y ) (x,y) (x,y)的状态dp[x][y]取决于dp[x-1][y]dp[x][y-1]。只要坐标 ( x − 1 , y ) (x-1,y) (x1,y) ( x , y − 1 ) (x,y-1) (x,y1)有一个可达,坐标 ( 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
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值