【LeetCode 热题 100】64. 最小路径和——(解法二)递推
Problem: 64. 最小路径和
文章目录
- 整体思路
- 完整代码
- # 时空复杂度
- 时间复杂度:O(m * n)
- 空间复杂度:O(m * n)
整体思路
这段代码同样旨在解决 “最小路径和” 问题,但它采用的是一种 自底向上(Bottom-Up)的动态规划 方法,也称为 迭代法 或 制表法 (Tabulation)。这种方法通过构建一个DP表(dp
二维数组),从起点开始,逐步计算出到达每个格子的最小路径和,最终得到终点的答案。
该实现通过巧妙地增加DP数组的维度并设置哨兵值,将边界条件的处理融入到统一的状态转移方程中,使得代码结构清晰。
-
状态定义与索引映射:
- 算法定义了一个二维DP数组
dp
,大小为(m+1) x (n+1)
。 dp[i][j]
的含义是:从起点grid[0][0]
到达网格中的点grid[i-1][j-1]
的最小路径和。- 这是一个索引偏移的技巧。
dp
数组的索引(i, j)
对应了m x n
网格中的索引(i-1, j-1)
。
- 算法定义了一个二维DP数组
-
基础情况与边界处理 (Base Cases & Boundary Handling):
- 设置“墙壁”:代码首先将
dp
数组的第0行和第0列(除了dp[0][0]
或dp[1][0]
等)填充为Integer.MAX_VALUE
。这相当于在网格的上方和左方建立了一堵无限高的“墙”,确保路径只能从有效的格子内部过来,而不会从“网格外部”以一个较小的值(比如0)进入计算,从而干扰Math.min
的结果。 - 设置起点:
if (i == 0 && j == 0)
这个条件在循环内部处理了起点。dp[1][1]
被设置为grid[0][0]
的值。这是整个DP计算的起点。
- 设置“墙壁”:代码首先将
-
状态转移 (State Transition):
- 算法使用两层嵌套循环来填充DP表。循环变量
i
和j
对应的是m x n
网格的索引。 - 在循环的每一步,计算
dp[i+1][j+1]
的值。 - 状态转移方程是
dp[i + 1][j + 1] = Math.min(dp[i][j + 1], dp[i + 1][j]) + grid[i][j]
。 - 我们来解析这个方程:
- 要到达
grid[i][j]
(对应dp[i+1][j+1]
),只能从其上方的grid[i-1][j]
(对应dp[i][j+1]
)或左方的grid[i][j-1]
(对应dp[i+1][j]
)过来。 - 因此,到达
grid[i][j]
的最小路径和,等于grid[i][j]
的值加上“到达上方格子的最小路径和”与“到达左方格子的最小路径和”中的较小者。 - 由于我们在第0行和第0列设置了极大值,当
i=0
时,dp[i][j+1]
(即dp[0][j+1]
) 是极大值,Math.min
会自动选择dp[i+1][j]
,正确地处理了第一行的情况。对第一列同理。
- 要到达
- 算法使用两层嵌套循环来填充DP表。循环变量
-
返回结果:
- 当所有循环结束后,
dp
数组已经完全填充。我们需要的最终答案,即到达终点grid[m-1][n-1]
的最小路径和,就存储在dp[m][n]
中。
- 当所有循环结束后,
完整代码
import java.util.Arrays;class Solution {/*** 计算从 m x n 网格左上角到右下角的最小路径和。* @param grid 包含非负整数的二维网格* @return 最小路径和*/public int minPathSum(int[][] grid) {int m = grid.length;int n = grid[0].length;// dp[i][j]: 表示从起点到网格 (i-1, j-1) 的最小路径和。// 使用 m+1, n+1 的大小是为了用第0行和第0列作为边界“墙”。int[][] dp = new int[m + 1][n + 1];// 初始化边界:将第0行设为极大值,作为上方的“墙”。Arrays.fill(dp[0], Integer.MAX_VALUE);// 遍历原始网格的行for (int i = 0; i < m; i++) {// 初始化边界:将每行的第0列设为极大值,作为左方的“墙”。dp[i + 1][0] = Integer.MAX_VALUE;// 遍历原始网格的列for (int j = 0; j < n; j++) {// 处理起点 (0,0) 的特殊情况if (i == 0 && j == 0) {// dp[1][1] 对应网格 (0,0),其最小路径和就是它自身的值。dp[1][1] = grid[i][j];} else {// 状态转移方程:// 到达网格 (i,j) 的最小路径和,等于// min(到达上方格子的最小和, 到达左方格子的最小和) + 当前格子的值。// dp[i][j+1] -> 上方格子// dp[i+1][j] -> 左方格子dp[i + 1][j + 1] = Math.min(dp[i][j + 1], dp[i + 1][j]) + grid[i][j];}}}// 最终答案是到达网格 (m-1, n-1) 的路径数,对应 dp[m][n]。return dp[m][n];}
}
# 时空复杂度
时间复杂度:O(m * n)
- 循环结构:算法使用了两层嵌套的
for
循环。- 外层循环从
i = 0
运行到m-1
,执行m
次。 - 内层循环从
j = 0
运行到n-1
,执行n
次。
- 外层循环从
- 计算依据:
- 总的操作次数是内外层循环次数的乘积,即
m * n
。初始化边界的操作复杂度是 O(m+n),被m*n
主导。
- 总的操作次数是内外层循环次数的乘积,即
综合分析:
算法的时间复杂度为 O(m * n)。
空间复杂度:O(m * n)
- 主要存储开销:算法创建了一个名为
dp
的二维整型数组来存储动态规划的所有中间状态。 - 空间大小:该数组的大小是
(m + 1) * (n + 1)
。
综合分析:
算法所需的额外空间主要由 dp
数组决定,其空间复杂度为 O(m * n)。