1. 题目分析
题目链接选自力扣 : 最小路径和
前面解触了很多路径题. 这依然是一个路径题, 题目要求从左上角到达右下角, 期间每次只能向右或者向下走一步, 求最终路径总和最小值.
2. 状态表示
以 ( i, j ) 位置结尾, 从起始位置到达 ( i, j ) 位置的最小路径和即为 dp[i][j]
3. 状态转移方程
根据之前的经验, 利用最近一步来划分问题. 对于路径问题想必已经是很熟悉了
- 从 ( i-1, j ) 位置到达 ( i, j ) 位置
从起始位置到达 ( i-1, j ) 位置后再多走一步到达 ( i,j ) 位置. 期间不同的路径到达 ( i-1, j ) 有不同的路径和, 选择最小的路径和后再多走一步到达 ( i, j ) 位置. 则此时最小路径和为到达 ( i-1, j ) 位置的最小路径和 dp[i-1][j] 在多走一步加上 grid[i][j]
- 从 ( i, j-1 ) 位置到达 ( i, j) 位置
同理, 此时最小路径和为 dp[i][j-1] + grid[i][j]
我们找的是到达 ( i, j ) 位置时的最下路径和, 因此是取这两种情况的最小值. 最终的状态转移方程为 : dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
4. 初始化
初始化为了保证状态转移方程填表时不越界. 根据状态转移方程可以看出, 这些位置是需要进行初始化的, 否则填表时会发生错误.
还是用新开一行一列的方式进行初始化, 通过外部新增的一行和一列来保证原本需要初始化的第一行和第一列在填表时正确.
在这题中, 初始化有一些细节情况
- 当填写下面这个位置时, 可以知道它受到我们新开的一行和一列中的两个影响. 根据动态转移方程,这个位置最终应该初始化为它本身的值. 因此就要求它上面和前面的位置不能影响它. 进而推导出这两个位置的值应该为 0
当填写下面这个位置时, 它受到这两个位置的影响. 根据状态转移方程取的是这两个位置的最小值. 但是新开的这个位置是为了来初始化它的不应该影响它的取值. 因此当这个位置取位正无穷时, 最小值就只能取这个位置的上方. 只受上方位置的影响.
最终我们发现, 除了起始位置的上方和后方初始化为 0, 其余新增的位置都应该初始化为无穷大才能保证填表时数据的正确性.
5. 填表顺序
根据状态转移方程, 想填写某个位置时, 需要先知道它上面一个位置和前一个位置的值. 因此填表的顺序为 从上往下填写每一行, 每一行从左往右填写.
6. 返回值
根据题目要求, 返回从起始点到达终点 ( m, n ) 的位置的路径最小和. 由于我们新开了一行一列, 对应到我们的状态表示中为 dp[m][n] ( 注意下标关系 )
7. 代码演示
class Solution { public int minPathSum(int[][] grid) { // 1. 创建 dp 表 int m = grid.length; int n = grid[0].length; int[][] dp = new int[m + 1][n + 1]; // 防止后续初始化 dp[1][0] 或者 dp[0][1] 时越界 // 此处只能用 &&, 当 m = 1 或 n = 1时, 只能代表该二维数组为一行或一列 // 一行时, 起始位置为第一个元素, 终点为最后一个元素 // 一列时, 同样起点为第一个元素, 终点为这一列最后一个元素 // 这里是针对二维数组只有一个元素时, 直接返回这个元素 if(m == 1 && n == 1) { return grid[0][0]; } // 2. 初始化 // 先将新增的第一行和第一列全部初始化为无穷大 for(int j = 0; j <= n; j++) { dp[0][j] = Integer.MAX_VALUE; } for(int i = 0; i <= m; i++) { dp[i][0] = Integer.MAX_VALUE; } // 将起始位置的上方和前方初始化为 0 dp[0][1] = dp[1][0] = 0; // 3. 填写 dp 表 for(int i = 1; i <= m; i ++) { for(int j = 1; j <= n; j++) { // 根据状态方程填写 // 由于我们新开了一行一列. 此时的位置对应到grid 中下标要 - 1 dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i - 1][j - 1]; } } // 4. 确认返回值 return dp[m][n]; } }