题目
给定一个包含非负整数的m x n网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1, 2, 3], [4, 5, 6]]
输出:12
记忆化递归法
记忆化递归法的核心思想是将递归过程中遇到的子问题及其解存储起来,即记忆化,以便在后续遇到相同子问题时直接查表获取结果,避免重复计算。针对本题,我们将使用一个二维数组memo来存储到达每个格子的最小路径和。使用记忆化递归法求解本题的主要步骤如下。
1、初始化记忆数组。创建一个与原网格大小相同的二维数组memo,全部初始化为一个特殊值(如None或极大值),表示尚未计算。
2、定义递归函数。编写一个递归函数,输入参数为当前坐标(i, j),该函数计算从(i, j)到右下角的最小路径和。递归的基本情况是当(i, j)位于右下角时,直接返回该格子的值。
3、记忆化处理。在递归函数内部,首先检查memo[i][j]是否已被计算过,若已计算则直接返回该值。
4、递归计算。否则,计算最小路径和,即分别计算从当前位置向下和向右移动的最小路径和,取二者中较小者加上当前位置的值。
5、更新记忆数组。将计算出的最小路径和存入memo[i][j]。
6、调用递归函数。从起点(0, 0)开始,调用递归函数。
根据上面的算法步骤,我们可以得出下面的示例代码。
def minimum_path_sum_by_recursion(grid):
def helper(i, j):
# 基本情况:到达右下角
if i == m - 1 and j == n - 1:
return grid[i][j]
# 已经计算过的情况,直接返回结果
if memo[i][j] is not None:
return memo[i][j]
# 向下或向右移动的最小路径和
if i < m - 1 and j < n - 1:
memo[i][j] = grid[i][j] + min(helper(i+1, j), helper(i, j+1))
elif i < m - 1:
# 只能向下
memo[i][j] = grid[i][j] + helper(i+1, j)
else:
# 只能向右
memo[i][j] = grid[i][j] + helper(i, j+1)
return memo[i][j]
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
# 初始化记忆数组
memo = [[None]*n for _ in range(m)]
# 从起点开始调用递归函数
return helper(0, 0)
grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]]
print(minimum_path_sum_by_recursion(grid))
grid = [[1, 2, 3], [4, 5, 6]]
print(minimum_path_sum_by_recursion(grid))
动态规划法
动态规划法的核心思想在于解决具有重叠子问题和最优子结构的问题。对于本题,我们要找的是从左上角到右下角的最小路径和。每一步只能向右或向下移动,这意味着到达任何一个格子的最小路径和,都是从其左边或上边的格子通过一步移动过来的最小路径和加上当前格子的值。因此,我们可以从左上角开始,逐步构建一个二维数组来存储到达每个格子的最小路径和。使用动态规划法求解本题的主要步骤如下。
1、初始化。创建一个与原网格相同大小的二维数组dp,其中dp[0][0] = grid[0][0],表示起点的最小路径和就是它本身的值。
2、边界条件。对于第一行的所有单元格,其最小路径和等于从上一列的相应单元格移动下来的值加上当前单元格的值。同理,第一列的所有单元格也是如此处理。
3、状态转移方程。对于dp[i][j](其中i>0且j>0),其值应为min(dp[i-1][j], dp[i][j-1]) + grid[i][j],即到达当前格子的最小路径和等于其上方格子和左侧格子的最小路径和中的较小者,再加上当前格子的值。
4、最终结果。dp[m-1][n-1]即为从左上角到右下角的最小路径和。
根据上面的算法步骤,我们可以得出下面的示例代码。
def minimum_path_sum_by_dp(grid):
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
# 初始化dp数组
dp = [[0]*n for _ in range(m)]
# 初始化dp数组的第一行和第一列
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 根据状态转移方程填充dp数组
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
grid = [[1, 3, 1], [1, 5, 1], [4, 2, 1]]
print(minimum_path_sum_by_dp(grid))
grid = [[1, 2, 3], [4, 5, 6]]
print(minimum_path_sum_by_dp(grid))
总结
记忆化递归法和动态规划法的时间复杂度均为O(m*n),其中m和n分别是网格的行数和列数。这是因为,每个单元格最多被访问一次。两者的空间复杂度也一样,均为O(m*n), 需要一个与原网格大小相同的二维数组来存储中间结果。
动态规划法还可以通过使用一维数组(滚动数组)来优化空间复杂度至 O(n)。这是因为,每一行的计算只需要前一行的信息,而不需要保留整个二维数组。对于每行,我们只需维护一个长度为n的一维数组。这样在计算完一行后,可以复用该数组空间来存储下一行的计算结果。