网格中的最短路径

标签: 广度优先搜索 数组 矩阵

难度: Hard

给你一个 m * n 的网格,其中每个单元格不是 0(空)就是 1(障碍物)。每一步,您都可以在空白单元格中上、下、左、右移动。

如果您 最多 可以消除 k 个障碍物,请找出从左上角 (0, 0) 到右下角 (m-1, n-1) 的最短路径,并返回通过该路径所需的步数。如果找不到这样的路径,则返回 -1 。

示例 1:

输入: grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 1
输出:6
解释:
不消除任何障碍的最短路径是 10。
消除位置 (3,2) 处的障碍后,最短路径是 6 。该路径是 (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (3,2) -> (4,2).

示例 2:

输入:grid = [[0,1,1],[1,1,1],[1,0,0]], k = 1
输出:-1
解释:我们至少需要消除两个障碍才能找到这样的路径。

提示:

  • grid.length == m
  • grid[0].length == n
  • 1 <= m, n <= 40
  • 1 <= k <= m*n
  • grid[i][j] 是 0 或 1
  • grid[0][0] == grid[m-1][n-1] == 0

Submission

运行时间: 43 ms

内存: 16.7 MB

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        if len(grid) == 0 or len(grid[0]) == 0:
            return 0
        m = len(grid)
        n = len(grid[0])
        if k >= m + n - 3:
            return m + n - 2
        dx = [(0, 1), (0, -1), (-1, 0), (1, 0)]
        visited = set((0,0,k))
        q = deque([(0,0,k)])
        step = 0
        while q:
            size = len(q)
            step += 1
            for i in range(size):
                x, y, rest = q.popleft()
                for d in dx:
                    nx = x + d[0]
                    ny = y + d[1]
                    if nx < 0 or nx >= m or ny < 0 or ny >= n:
                        continue 
                    if grid[nx][ny] == 0 and (nx, ny, rest) not in visited:
                        if nx == m - 1 and ny == n - 1:
                            return step
                        q.append((nx, ny, rest))
                        visited.add((nx, ny, rest))
                    elif grid[nx][ny] == 1 and rest > 0 and (nx, ny, rest - 1) not in visited:
                        q.append((nx, ny, rest-1))
                        visited.add((nx, ny, rest-1))
        return -1


Explain

这题解使用了宽度优先搜索(BFS)来找到从网格的左上角到右下角的最短路径。这里的关键是可以选择消除最多k个障碍物。为了处理这个条件,搜索状态不仅包括当前位置坐标 (x, y),还包括一个剩余障碍物消除次数 `rest`。状态被保存在一个队列中,每个状态为 (x, y, rest),其中 `rest` 表示还可以消除的障碍物数量。为防止重复处理相同的状态,使用一个集合 `visited` 来记录已经访问过的状态。搜索过程中,对于每个状态,都尝试向四个方向移动,如果是空白格并且该状态未被访问,则将其加入队列;如果是障碍物且 `rest` 大于0,则消除障碍物并将新状态加入队列。如果能到达终点,则返回当前步数。

时间复杂度: O(m*n*k)

空间复杂度: O(m*n*k)

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        if len(grid) == 0 or len(grid[0]) == 0:
            return 0  # 如果网格为空,直接返回0
        m = len(grid)
        n = len(grid[0])
        if k >= m + n - 3:
            return m + n - 2  # 如果可以移除的障碍物足够多,直接走最短路径
        dx = [(0, 1), (0, -1), (-1, 0), (1, 0)]  # 方向数组
        visited = set((0,0,k))  # 访问集合
        q = deque([(0,0,k)])  # BFS 队列
        step = 0
        while q:
            size = len(q)
            step += 1
            for i in range(size):
                x, y, rest = q.popleft()
                for d in dx:
                    nx = x + d[0]
                    ny = y + d[1]
                    if nx < 0 or nx >= m or ny < 0 or ny >= n:
                        continue  # 越界检查
                    if grid[nx][ny] == 0 and (nx, ny, rest) not in visited:
                        if nx == m - 1 and ny == n - 1:
                            return step  # 如果到达终点,返回步数
                        q.append((nx, ny, rest))  # 加入新状态
                        visited.add((nx, ny, rest))
                    elif grid[nx][ny] == 1 and rest > 0 and (nx, ny, rest - 1) not in visited:
                        q.append((nx, ny, rest-1))  # 消除障碍物并加入新状态
                        visited.add((nx, ny, rest-1))
        return -1  # 如果无法到达终点,返回-1

Explore

如果起始位置`(0, 0)`是障碍物,且障碍消除次数`k`大于0,则可以将其视为一个可通过的位置并开始搜索。如果`k`等于0,则无法开始搜索,返回-1。对于终点位置`(m-1, n-1)`,如果它是障碍物并且在到达该位置时还有剩余的障碍消除次数,则可以消除这个障碍物并到达终点。如果没有剩余次数,即使到达终点附近也无法完成任务,返回-1。

这个判断基于最短路径的理论计算,其中最短路径是从左上角到右下角的直线路径,这个路径的长度为`m + n - 2`。在这条路径中,除了起始点和终点外,最多有`m + n - 3`个其他点可能是障碍物。因此,如果`k`的值大于或等于`m + n - 3`,意味着可以移除路径上所有可能的障碍物,直接沿着最短路径走到终点。

使用`(x, y, rest)`作为状态让算法能够区分在不同剩余障碍物消除次数下的位置状态。这样做的优势在于可以有效处理每个位置在不同消除次数下的访问情况,防止因障碍物而无法进一步探索的情况。然而,这也带来了内存使用增加的问题,因为状态空间变大,相应的`visited`集合也需要存储更多元素,增加了内存占用。

`visited`集合通过记录已访问的状态,帮助避免重复处理相同状态,从而提高算法效率。然而,这种方法可能导致高内存消耗,因为每个不同的`(x, y, rest)`组合都需要被记录。此外,如果`k`值很大,`visited`集合的大小会显著增加,可能会导致内存溢出或降低处理速度。