LeetCode 64:最小路径和——Java 原地动态规划详解

一、题目描述

给定一个包含非负整数的 m × n 网格 grid,需要从左上角出发,到达右下角,并使经过路径上的数字总和最小。

每次只能执行以下两种操作之一:

  • 向右移动一格;

  • 向下移动一格。

例如:

复制代码
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:最小路径为 1 → 3 → 1 → 1 → 1,路径和为 7。

另一个示例:

复制代码
输入:grid = [[1,2,3],[4,5,6]]
输出:12

本题和"不同路径"很相似,但"不同路径"统计的是路径数量,而本题需要在所有可行路径中找到数字总和最小的一条路径。


二、为什么使用动态规划?

假设机器人已经到达位置 (i,j)。由于它只能向右或向下移动,所以进入当前位置前,只可能位于:

  1. 上方位置 (i-1,j),然后向下走一步;

  2. 左方位置 (i,j-1),然后向右走一步。

如果已经知道从左上角到达这两个位置的最小路径和,那么只需要选择较小的一个,再加上当前格子的数字,就能得到到达当前位置的最小路径和。

较小位置的结果会被后续多个状态重复使用,因此可以利用动态规划保存已经计算出的最优结果。


三、定义动态规划状态

定义:

复制代码
dp[i][j]:从左上角 (0,0) 到达位置 (i,j) 的最小路径和

这里使用 Java 数组的 0-based 下标:

  • 左上角位置是 (0,0)

  • 右下角位置是 (row-1,col-1)

  • 最终答案是 dp[row-1][col-1]

例如,dp[2][1] 表示从左上角走到第 3 行、第 2 列时,能够取得的最小路径和。


四、确定边界状态

1. 左上角

起点没有前置位置,路径和就是当前格子的值:

复制代码
dp[0][0] = grid[0][0];

如果直接复用 grid 数组,这个位置不需要修改,因为 grid[0][0] 本身就是起点的最小路径和。

2. 初始化第一行

机器人位于第一行时不能从上方进入,只能从左侧一路向右走。因此:

复制代码
dp[0][j] = dp[0][j - 1] + grid[0][j];

原地修改 grid 时写成:

复制代码
for (int j = 1; j < col; j++) {
    grid[0][j] += grid[0][j - 1];
}

3. 初始化第一列

机器人位于第一列时不能从左侧进入,只能从上方一路向下走。因此:

复制代码
dp[i][0] = dp[i - 1][0] + grid[i][0];

原地修改代码为:

复制代码
for (int i = 1; i < row; i++) {
    grid[i][0] += grid[i - 1][0];
}

先处理第一行和第一列,可以为后续状态转移准备完整的边界数据。


五、推导状态转移公式

对于不在第一行、也不在第一列的位置 (i,j),存在两种进入方式。

1. 从上方进入

从左上角到上方位置 (i-1,j) 的最小路径和为:

复制代码
dp[i - 1][j]

再向下进入当前位置,需要加上 grid[i][j]

2. 从左方进入

从左上角到左方位置 (i,j-1) 的最小路径和为:

复制代码
dp[i][j - 1]

再向右进入当前位置,同样需要加上 grid[i][j]

为了让路径和最小,需要选择两个前置状态中较小的一个:

复制代码
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1])
        + grid[i][j];

核心思路可以概括为:

当前位置的最小路径和,等于上方与左方最小路径和中的较小值,再加上当前位置的数字。


六、为什么可以直接修改 grid

按照标准定义,通常需要另外创建一个 dp 数组。但观察状态转移可以发现:计算完成后,原始的 grid[i][j] 不会再单独使用,它只会作为当前状态的一部分参与一次计算。

因此,可以直接让:

复制代码
grid[i][j] = dp[i][j]

此时,已经计算过的位置不再保存原始权重,而是保存从左上角到达该位置的最小路径和。状态转移可以改写为:

复制代码
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);

这种写法不需要额外二维数组,将额外空间复杂度降为 O(1)

但它有一个明显副作用:方法执行后,传入的 grid****会被修改 。如果调用方后续还需要使用原始网格,就应该创建独立的 dp 数组,或者先复制 grid,不能盲目使用原地修改。


七、示例完整推演

输入网格为:

复制代码
1  3  1
1  5  1
4  2  1

1. 初始化第一行

第一行只能从左向右累加:

复制代码
grid[0][1] = 3 + 1 = 4
grid[0][2] = 1 + 4 = 5

得到:

复制代码
1  4  5
1  5  1
4  2  1

2. 初始化第一列

第一列只能从上向下累加:

复制代码
grid[1][0] = 1 + 1 = 2
grid[2][0] = 4 + 2 = 6

得到:

复制代码
1  4  5
2  5  1
6  2  1

3. 计算其他位置

计算 (1,1)

复制代码
grid[1][1] = 5 + min(4, 2) = 7

计算 (1,2)

复制代码
grid[1][2] = 1 + min(5, 7) = 6

计算 (2,1)

复制代码
grid[2][1] = 2 + min(7, 6) = 8

计算 (2,2)

复制代码
grid[2][2] = 1 + min(6, 8) = 7

最终网格变成:

复制代码
1  4  5
2  7  6
6  8  7

右下角 grid[2][2] = 7,所以最小路径和为 7

需要注意,修改后的每个数字表示的是"到达当前位置的最小路径和",已经不再是原网格中的数字。


八、Java 完整代码

复制代码
class Solution {

    public int minPathSum(int[][] grid) {
        int row = grid.length;
        int col = grid[0].length;

        // 第一行只能从左边到达,因此累加左侧状态
        for (int j = 1; j < col; j++) {
            grid[0][j] += grid[0][j - 1];
        }

        // 第一列只能从上方到达,因此累加上方状态
        for (int i = 1; i < row; i++) {
            grid[i][0] += grid[i - 1][0];
        }

        // 其他位置可以从上方或左方到达
        for (int i = 1; i < row; i++) {
            for (int j = 1; j < col; j++) {
                grid[i][j] += Math.min(
                        grid[i - 1][j],
                        grid[i][j - 1]
                );
            }
        }

        return grid[row - 1][col - 1];
    }
}

代码执行过程如下:

  1. 获取网格的行数和列数;

  2. 累加第一行,建立只能向右走的边界状态;

  3. 累加第一列,建立只能向下走的边界状态;

  4. (1,1) 开始计算其余位置;

  5. 返回右下角保存的最小路径和。


九、为什么遍历顺序必须从左上到右下?

计算 grid[i][j] 时依赖:

复制代码
grid[i - 1][j]  // 上方状态
grid[i][j - 1]  // 左方状态

因此,在计算当前位置之前,上方和左方必须已经完成更新。按照从上到下、每行从左到右的顺序遍历,正好满足这个依赖关系。

如果从右下角反向遍历,却仍使用相同公式,就可能读取尚未转换成最小路径和的原始值,导致结果错误。


十、复杂度分析

时间复杂度

每个网格位置最多被访问一次,因此时间复杂度为:

复制代码
O(m × n)

空间复杂度

代码直接复用输入数组,只使用了少量局部变量,因此额外空间复杂度为:

复制代码
O(1)

这里的 O(1) 指额外空间,不代表输入网格本身只占常量空间。

相关推荐
Tisfy1 小时前
LeetCode 1386.安排电影院座位:哈希表+位运算
算法·leetcode·散列表·题解·哈希表
Nil2082 小时前
leetcode 199二叉树的右视图
算法·leetcode·深度优先
凤山老林2 小时前
Spring Boot 3.x AOT 编译实战:从原理、踩坑到生产落地
java·spring boot·后端
博傅2 小时前
Spring 核心原理
java·后端·spring
yurenpai(27届找实习中)3 小时前
从零读懂 AI 智能客服(一):模块职责与 SSE 聊天链路(后端架构)
java·人工智能·架构·langchain4j
陈皮波比茶3 小时前
Swagger
java
小闫BI设源码3 小时前
Elasticsearch面试必看:如何让客户端精准选择节点高效执行请求?
java·elasticsearch·面试宝典·深入解析
摇滚侠4 小时前
《SpringBoot 3:入门与应用实战》第 5 章 使用 Spring Boot 阅读笔记 9
java·spring boot·笔记
进阶的小名4 小时前
Spring AI 2.0 探索:多 OpenAI-Compatible 模型接入,以及下一代 Session 记忆管理
java·人工智能·后端·gpt·spring·ai·chatgpt