网格图中最少访问的格子数

标签: 广度优先搜索 并查集 数组 动态规划 矩阵 单调栈 堆(优先队列)

难度: Hard

给你一个下标从 0 开始的 m x n 整数矩阵 grid 。你一开始的位置在 左上角 格子 (0, 0) 。

当你在格子 (i, j) 的时候,你可以移动到以下格子之一:

  • 满足 j < k <= grid[i][j] + j 的格子 (i, k) (向右移动),或者
  • 满足 i < k <= grid[i][j] + i 的格子 (k, j) (向下移动)。

请你返回到达 右下角 格子 (m - 1, n - 1) 需要经过的最少移动格子数,如果无法到达右下角格子,请你返回 -1 。

示例 1:

输入:grid = [[3,4,2,1],[4,2,3,1],[2,1,0,0],[2,4,0,0]]
输出:4
解释:上图展示了到达右下角格子经过的 4 个格子。

示例 2:

输入:grid = [[3,4,2,1],[4,2,1,1],[2,1,1,0],[3,4,1,0]]
输出:3
解释:上图展示了到达右下角格子经过的 3 个格子。

示例 3:

输入:grid = [[2,1,0],[1,0,0]]
输出:-1
解释:无法到达右下角格子。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 105
  • 1 <= m * n <= 105
  • 0 <= grid[i][j] < m * n
  • grid[m - 1][n - 1] == 0

Submission

运行时间: 662 ms

内存: 47.4 MB

class Solution:
    def minimumVisitedCells(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        m1, n1 = m - 1, n - 1
        col_st = [[] for _ in range(n1)] + [[(m1, 1)]]
        st = [(n1, 1)]
        for j in range(n1 - 1, -1, -1):
            p = st_remove(st, j + grid[m1][j])
            if p == inf:
                continue
            st.append((j, p))
            col_st[j].append((m1, p))
        for i in range(m1 - 1, -1, -1):
            rst = []
            for j in range(n1, -1, -1):
                cst = col_st[j]
                p1, p2 = st_remove(cst, i + grid[i][j]), st_remove(rst, j + grid[i][j])
                if p2 < p1:
                    p1 = p2
                    st_remove_1(cst, p1)
                elif p2 > p1:
                    st_remove_1(rst, p1)
                elif p1 == inf:
                    continue
                cst.append((i, p1))
                rst.append((j, p1))
        st = col_st[0]
        return st[-1][1] if len(st) > 0 and st[-1][0] == 0 else -1


def st_remove(st, k):
    p = len(st) - 1
    if p < 0 or k < st[p][0]:
        return inf
    while p > 0 and k >= st[p - 1][0]:
        p -= 1
    del st[p + 1:]
    return st[p][1] + 1

def st_remove_1(st, k):
    p = len(st) - 1
    while p >= 0 and k <= st[p][1]:
        p -= 1
    del st[p + 1:]

Explain

这个题解采用的是动态规划的思想,结合一种特殊的数据结构来优化搜索过程。从网格的右下角开始反向思考到达左上角的最小步数,使用两个列表来维护可能的移动路径和步数。对于每一个格子,计算向左和向上移动的最小步数,如果这个步数小于当前记录的步数,则更新。这种方法通过从右下到左上的方式,逐步缩减搜索空间,并在过程中不断更新每个格子到达终点的最小步数。

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

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

class Solution:
    def minimumVisitedCells(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        m1, n1 = m - 1, n - 1
        col_st = [[] for _ in range(n1)] + [[(m1, 1)]] # 列状态初始化
        st = [(n1, 1)] # 状态列表初始化
        for j in range(n1 - 1, -1, -1):
            p = st_remove(st, j + grid[m1][j])
            if p == inf:
                continue
            st.append((j, p)) # 更新状态列表
            col_st[j].append((m1, p)) # 更新列状态
        for i in range(m1 - 1, -1, -1):
            rst = [] # 行状态清空准备更新
            for j in range(n1, -1, -1):
                cst = col_st[j]
                p1, p2 = st_remove(cst, i + grid[i][j]), st_remove(rst, j + grid[i][j])
                if p2 < p1:
                    p1 = p2
                    st_remove_1(cst, p1)
                elif p2 > p1:
                    st_remove_1(rst, p1)
                elif p1 == inf:
                    continue
                cst.append((i, p1))
                rst.append((j, p1))
        st = col_st[0]
        return st[-1][1] if len(st) > 0 and st[-1][0] == 0 else -1

def st_remove(st, k):
    p = len(st) - 1
    if p < 0 or k < st[p][0]:
        return inf
    while p > 0 and k >= st[p - 1][0]:
        p -= 1
    del st[p + 1:]
    return st[p][1] + 1

def st_remove_1(st, k):
    p = len(st) - 1
    while p >= 0 and k <= st[p][1]:
        p -= 1
    del st[p + 1:]

Explore

从右下角开始反向思考到达左上角的最小步数的优势在于可以直接处理目标格子(右下角)的初始化问题,并从结束点开始逐步向起始点推进,这样的计算逻辑使得每一步都建立在已经计算过的结果之上。这种反向方式有助于减少中间状态的存储和处理,特别是在有些情况下,从终点到起点的路径可能比从起点到终点更容易处理或者更直观。此外,对于一些特定类型的问题,这种方式可能会更自然地适应问题的边界条件和限制。

在题解中,维护的两个列表分别是列状态列表(col_st)和行状态列表(rst)。列状态列表用于存储每列的状态,行状态列表用于存储当前处理行的状态。这些列表在算法中通过动态地添加和删除元素来维护。具体地,对于每一列或行,在进行动态规划计算时,会根据该列或行的特定格子和之前计算的状态,来决定是否更新步数。更新操作涉及到根据当前格子的行动成本以及之前的状态来计算新的最小步数,并将这个状态添加到列表中。同时,为了维持状态的最优性,会移除那些不再可能成为最优解的旧状态。这种动态的更新保证了每一步的计算都是基于最新的最优状态进行的。

在题解中使用的动态规划的状态定义为每个格子到达终点(右下角)的最小步数。具体来说,状态可以表示为一个二元组 (i, p),其中 i 表示当前考虑的列或行的索引,p 表示从当前位置到终点的最小步数。动态规划的转移过程则是基于从一个格子到其相邻格子(向上或向左)的移动成本,来更新这些状态。对于每个格子,算法会考虑从当前格子移动到上方或左侧格子的成本,然后根据这些成本以及从这些相邻格子到终点的已知最小步数,来更新当前格子到终点的最小步数。这样的状态转移保证了每一步的更新都是向着减少总步数的方向进行的。