【动态规划: 路径问题】下降路径最小和

文章目录

题目链接:931. 下降路径最小和


题目解析

动态规划 (Dynamic programming,简称 DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,在查找 有很多重叠子问题 的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费不必要的时间。

通常的解决步骤有五步:

  1. 确定状态表示/dp表的含义
  2. 确定状态转移方程/递推公式
  3. 初始化
  4. 确定填表顺序
  5. 确定返回值

给你一个 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. 取示例1的结束位置 frame[2][2] 为例,到达这个位置时的不同路径数怎么算?
  2. 根据题目所说,我们可以往正下方走,也可以沿着对角线往左或者往右走,因此,我们分类讨论:1. 从它的左上方往右下方走,此时的最小路径和等于其左上方位置对应的最小路径和 dp[1][1] 再加上该位置的值 matrix[2][2];2. 从它的正上方往下方走,此时的最小路径和等于其正上方位置对应的最小路径和 dp[1][2] 再加上该位置的值 matrix[2][2];3. 从它的右上方往左下方走,此时的最小路径和等于其右上方位置对应的最小路径和 dp[1][3] 再加上该位置的值 matrix[2][2]。
  3. 然后在这三种情况中找到最小值。
  4. 因此:到某个位置时,最小路径和为 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):

为什么这样修改呢?

  1. 在填第一行的时候,由于我们需要保持 dp 的值填完后是和 matrix 中对应的值一致的,因此第0行初始化为 0。
  2. 在填其他行的时候,我们需要从其左上方、正上方和右上方的 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, +♾️}。

填表时:

  1. 填第一行第一个,原本是 dp[1][1] = dp[0][0](左上) + dp[0][1](上) + dp[0][2](右上),现在改为 dp[1] = dp[0] + dp[1] + dp[2],
  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;
    }
}

相关推荐
秋饼1 小时前
MySQL 分库分表完全指南:ShardingSphere-JDBC 实战、分片键选型、场景边界、20 个生产坑与 30 道面试点
java·mysql·面试·shardingsphere·分库分表·后端架构
郭小李1 小时前
spring 的 @CookieValue 注解 用法
java·后端·spring
天远Date Lab2 小时前
零信任架构实战:基于天远企业股权变更构建自动化招投标供应商股东穿透审查网关
java·人工智能·架构·自动化
deepseek232 小时前
OpenAI三项科学发现拆解:3-SAT亚指数算法挑战ETH,AI科学声明的验证流水线怎么搭
人工智能·算法·openai
梦帮科技2 小时前
基于幺正李代数连续归一化流的混合量子生成模型 (HQNN-Flow) 完整技术指南(上部分)
网络·人工智能·算法·量子计算
青少儿编程课堂2 小时前
蓝桥杯青少组 + 全国青少年信息素养大赛 + 电子学会图形化考级|校园推箱子大挑战解析
c++·python·算法·bfs·信息学竞赛
ynchyong2 小时前
培养程序化思维
leetcode·程序化思维·刷题经验
weixin_399380692 小时前
TongWeb7.0.4.9M11如何接入到idea(by lqw)
java·ide·intellij-idea
paopaokaka_luck2 小时前
社区服务平台(加权协同过滤推荐、Jieba中文词频分析、WebSocket实时聊天、求助处理闭环、 活动积分闭环、ECharts多维分析)
java·前端·spring boot·mysql·echarts·mybatis