文章目录
题目链接:931. 下降路径最小和
题目解析
动态规划 (Dynamic programming,简称 DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,在查找 有很多重叠子问题 的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费不必要的时间。
通常的解决步骤有五步:
- 确定状态表示/dp表的含义
- 确定状态转移方程/递推公式
- 初始化
- 确定填表顺序
- 确定返回值
给你一个 n x n 的 方形 整数数组 matrix,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
示例1:matrix = [[2,1,3],[6,5,4],[7,8,9]]

如图所示为两条最小下降路径,路径和均为 13,因此返回 13。
示例2:matrix = [[-19,57],[-40,-5]]

如图所示,最小路径和为 -59,返回 -59 即可。
算法原理
确定状态表示
状态表示/dp表含义可以从题目要求找出,也可以从重复子问题中找出。
通常可以确定 以 i 位置为起始 或者 以 i 位置为结尾。
本题采用后者,因此状态表示:dpij 表示到 i, j 位置时的最小路径和。
确定状态转移方程
我们随机取一个情况,从最近的一步来划分问题思考:
- 取示例1的结束位置
frame[2][2]为例,到达这个位置时的不同路径数怎么算? - 根据题目所说,我们可以往正下方走,也可以沿着对角线往左或者往右走,因此,我们分类讨论:1. 从它的左上方往右下方走,此时的最小路径和等于其左上方位置对应的最小路径和
dp[1][1]再加上该位置的值matrix[2][2];2. 从它的正上方往下方走,此时的最小路径和等于其正上方位置对应的最小路径和dp[1][2]再加上该位置的值matrix[2][2];3. 从它的右上方往左下方走,此时的最小路径和等于其右上方位置对应的最小路径和dp[1][3]再加上该位置的值matrix[2][2]。 - 然后在这三种情况中找到最小值。
- 因此:到某个位置时,最小路径和为
dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i-1][j+1])) + matrix[i][j],这就是状态转移方程。
初始化
当我们填dp表时,填 dp[1][1] 的时候需要其左上方、正上方和右上方的三个越界的格子,因此我们需要将 dp 表的大小改成 (m+2)x(n+1):

为什么这样修改呢?
- 在填第一行的时候,由于我们需要保持 dp 的值填完后是和 matrix 中对应的值一致的,因此第0行初始化为
0。 - 在填其他行的时候,我们需要从其左上方、正上方和右上方的 dp 值中找到最小的,因此不能让 0 来捣乱(例如填 dp21 的时候,其左上方、正上方和右上方的 dp 值分别是 0,dp11,dp13,此时若 dp11 和 dp13 的值是正数,就会被 0 影响, dp21 将等于 0 + matrix**1** **0** ),所以要初始化为 无穷大。
- 在填 dp表的时候,我们需要判断坐标对应在
matrix中的值,但是dp的大小和matrix的大小不同,因此在访问matrix的时候要特别注意 下标的映射关系
确定填表顺序
按照 从上往下、从左往右 的顺序访问网格,填 dp表(二维数组)。
确定返回值
我们需要返回遍历完整个网格时的 最小路径和,由于我们可以从第一行的任意位置出发,结果会在最后一行中,因此遍历一遍最后一行找出最小值,返回即可。
代码实现
普通动态规划
java
class Solution {
public int minFallingPathSum(int[][] matrix) {
// 预处理
int m = matrix.length, n = matrix[0].length;
// 创建dp表:所有值初始化为正无穷大
int[][] dp = new int[m+1][n+2];
for (int i = 1; i <= m; i++) {
dp[i][0] = dp[i][n+1] = Integer.MAX_VALUE;
}
// 初始化:将第0行初始化为0
for (int j = 0; j <= n; j++) {
dp[0][j] = 0;
}
// 从上往下、从左往右填dp表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i-1][j+1])) + matrix[i-1][j-1];
}
}
// 扫描最后一行,找到最小值
int ans = dp[m][0];
for (int j = 1; j <= n; j++) {
ans = Math.min(ans, dp[m][j]);
}
// 返回结果
return ans;
}
}
空间优化版本
由于 dp[i][j] 只依赖 dp[i-1][j-1]、dp[i-1][j] 和 dp[i-1][j] 这三个值,我们可以不创建整个 dp 二维数组,只保留一行,用一维数组 dp[j] 来滚动覆盖。
初始的 dp[j] 数组表示 第0行 ,将 dp[0] 和 dp[n-1] 初始化为无穷大之后:dp = {+♾️, 0, 0, 0, +♾️}。
填表时:
- 填第一行第一个,原本是
dp[1][1] = dp[0][0](左上) + dp[0][1](上) + dp[0][2](右上),现在改为dp[1] = dp[0] + dp[1] + dp[2], - 接下来填第一行第二个,
dp[1][2] = dp[0][1](左上) + dp[0][2](上) + dp[0][3](右上),现在是dp[2] =dp1+ dp[1] + dp[2]。但是注意!我们此时期望dp[1]是 第0行的第1个 ------即原来的dp[0][1] = 0,但是第一行第一个填完之后,这个dp[1]已经被更新成 第1行的第一个 了,值被改为matrix[0][0] = 2,所以会出错。怎么解决呢?只需要在计算第一行第一个之前先将dp[1]的值(也就是dp[0][1])记录下来,等到计算完dp[1][1]之后,将记录的dp[0][1]赋值给prev,通过一个prev充当 左上方,留给后面列计算时使用。
此时状态转移方程改为:dp[j] = Math.min(prev, Math.min(dp[j], dp[j+1])) + matrix[i][j]。填表顺序依旧是 从上往下、从左往右。
java
class Solution {
public int minFallingPathSum(int[][] matrix) {
// 预处理
int m = matrix.length, n = matrix[0].length;
// 创建dp表:第0列和第n+1列初始化为正无穷大
int[] dp = new int[n+2];
dp[0] = dp[n+1] = Integer.MAX_VALUE;
// dp[1..n] 默认 0,代表虚拟第 0 行
// 从上往下、从左往右填dp表
for (int i = 1; i <= m; i++) {
int prev = dp[0]; // dp[i-1][j-1]
for (int j = 1; j <= n; j++) {
int cur = dp[j]; // dp[i-1][j]
dp[j] = Math.min(prev, Math.min(dp[j], dp[j+1])) + matrix[i-1][j-1];
// 继续下一行滚动时先将prev改为dp[i][j],对于下一行就是dp[i-1][j-1]
prev = cur;
}
}
// 扫描最后一行,找到最小值
int ans = dp[0];
for (int j = 1; j <= n; j++) {
ans = Math.min(ans, dp[j]);
}
// 返回结果
return ans;
}
}