LeetCode 64. 最小路径和

题目描述

给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和最小。

每次只能向下或者向右移动一步。

例如:

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

最初思路

一开始想到的是自顶向下的记忆化搜索:

复制代码
dfs(i,j):从 (0,0) 到达坐标 (i,j) 的最小路径和

因为只能向右或向下走,所以反过来看,到达 (i,j) 之前只可能来自两个位置:

复制代码
上方:(i - 1, j)
左方:(i, j - 1)

所以状态转移是:

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

再用 memo[i][j] 缓存已经计算过的最小路径和,避免重复递归。

问题出在哪里

第一版代码把越界状态写成了:

复制代码
if i < 0 or j < 0:
    return 0

这个返回值会让"不存在的路径"参与 min,并且因为 0 很小,很容易被错误选中。

比如:

复制代码
grid = [[1,2]]

计算 dfs(0,1) 时:

复制代码
dfs(0,1) = min(dfs(-1,1), dfs(0,0)) + grid[0][1]

如果 dfs(-1,1) 返回 0,那么它会比真实路径 dfs(0,0) 更小,结果就会少算 grid[0][0]

正确思路

继续沿用原来的记忆化搜索,核心是让边界状态的含义和状态定义一致:

复制代码
dfs(i,j):从左上角 (0,0) 到坐标 (i,j) 的最小路径和

边界条件:

复制代码
1. i < 0 或 j < 0:走出网格,是非法路径,返回一个足够大的值
2. i == 0 且 j == 0:到达起点,返回 grid[0][0]
3. memo[i][j] 已经计算过:直接返回缓存值

最终答案就是:

复制代码
dfs(m - 1, n - 1)

手推过程

用只有一行的用例检查越界返回值:

复制代码
grid = [[1,2]]

正确路径只能是:

复制代码
1 -> 2

所以答案应该是:

复制代码
3

如果越界返回 0

复制代码
dfs(0,1)
= min(dfs(-1,1), dfs(0,0)) + grid[0][1]
= min(0, 1) + 2
= 2

这里得到 2,说明非法路径被 min 选中了。

如果越界返回足够大的值:

复制代码
dfs(0,1)
= min(很大的值, dfs(0,0)) + grid[0][1]
= min(很大的值, 1) + 2
= 3

这样才会保留真实路径。

状态定义

复制代码
dfs(i,j):从 (0,0) 到达 (i,j) 的最小路径和。

这个定义的重点是:返回值已经包含当前位置 grid[i][j]

所以起点不是返回 0,而是返回:

复制代码
grid[0][0]

状态转移

到达 (i,j) 之前,只可能从上方或左方过来:

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

其中:

复制代码
dfs(i - 1,j):从上方走下来之前的最小路径和
dfs(i,j - 1):从左方走过来之前的最小路径和
grid[i][j]:当前位置也要加入路径和

初始化与递归边界

递归里有两个核心边界:

复制代码
越界:返回足够大的值,表示非法路径
起点:返回 grid[0][0]

也就是:

复制代码
if i < 0 or j < 0: return Integer.MAX_VALUE / 2
if i == 0 and j == 0: return grid[0][0]

这里用 Integer.MAX_VALUE / 2,是为了避免后面再加 grid[i][j] 时出现整数溢出。

另外,因为题目里的格子值是非负整数,最小路径和可能为 0。如果直接用 memo[i][j] > 0 判断是否计算过,在全是 0 的网格里缓存会失效。更稳的写法是先把 memo 初始化为 -1

伪代码

下面的伪代码与后面的 Java 实现一一对应:

复制代码
创建大小为 m x n 的 memo 数组
把 memo 中每个位置初始化为 -1
返回 dfs(m - 1, n - 1, grid, memo)

函数 dfs(i, j, grid, memo):
    如果 i < 0 或 j < 0:
        返回一个足够大的值

    如果 i == 0 且 j == 0:
        返回 grid[0][0]

    如果 memo[i][j] 不等于 -1:
        返回 memo[i][j]

    memo[i][j] = min(dfs(i - 1, j), dfs(i, j - 1)) + grid[i][j]
    返回 memo[i][j]

Java 代码

复制代码
import java.util.Arrays;

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] memo = new int[m][n];

        for (int[] row : memo) {
            Arrays.fill(row, -1);
        }

        return dfs(m - 1, n - 1, grid, memo);
    }

    public int dfs(int i, int j, int[][] grid, int[][] memo) {
        if (i < 0 || j < 0) {
            return Integer.MAX_VALUE / 2;
        }
        if (i == 0 && j == 0) {
            return grid[0][0];
        }
        if (memo[i][j] != -1) {
            return memo[i][j];
        }

        return memo[i][j] = Math.min(
            dfs(i - 1, j, grid, memo),
            dfs(i, j - 1, grid, memo)
        ) + grid[i][j];
    }
}

易错点

1. 越界不能返回 0

这题是在求最小值。

如果非法路径返回 0,它会变成一个很小的候选值,被 Math.min 错误选中。

越界状态应该表示"不可达",所以要返回一个足够大的值。

2. 起点要返回 grid[0][0]

dfs(i,j) 表示从 (0,0)(i,j) 的最小路径和。

起点本身就在路径里,所以:

复制代码
dfs(0,0) = grid[0][0]

如果返回 0,就会漏算起点的值。

3. 大数要避免溢出

如果越界直接返回:

复制代码
Integer.MAX_VALUE

后面再执行:

复制代码
Integer.MAX_VALUE + grid[i][j]

可能发生整数溢出。

所以更稳的是:

复制代码
Integer.MAX_VALUE / 2

4. memo 的哨兵值要避开合法答案

因为 grid 里的数字可以是 0,所以某些位置的最小路径和也可能是 0

如果用:

复制代码
memo[i][j] > 0

判断是否计算过,那么答案为 0 的状态不会命中缓存。

更稳的做法是把 memo 初始化为 -1,再用:

复制代码
memo[i][j] != -1

判断是否已经计算过。

建议测试用例

复制代码
grid = [[1]]
输出:1

grid = [[1,2]]
输出:3

grid = [[1],[2]]
输出:3

grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7

grid = [[0,0],[0,0]]
输出:0

复杂度分析

  • 时间复杂度:O(m * n),每个网格坐标最多被计算一次。
  • 空间复杂度:O(m * n),主要来自 memo 数组;递归调用栈最多为 O(m + n)

总结

这题的核心不是换解法,而是让边界返回值服务于 min 这个转移。

只要状态定义是:

复制代码
dfs(i,j):到达坐标 (i,j) 的最小路径和

那么越界就应该返回"大数"来表示非法路径,起点就应该返回 grid[0][0] 来计入起点值。

[[1,2]] 这种一行网格手推一遍,很容易发现越界返回 0 会把非法路径选进来。

相关推荐
find1star9 小时前
LeetCode 141:环形链表
java·算法·leetcode·链表
迅翼SwiftWing10 小时前
编队系列(总)|固定翼编队如何实现稳定飞行?通信架构、协同算法与工程约束深度解析
算法·架构
LB211210 小时前
力扣160 21 86
算法·leetcode·职场和发展
小智老师PMP11 小时前
2026深度解析|PMP第八版与NPDP核心侧重点本质区别(管理类证书怎么选)
开发语言·分布式·算法·职场和发展·产品经理
HugoStudio_SWAN11 小时前
洛谷 B4450 / B3867 / B3923 智慧购物、储蓄与做题——从程序到生活
c++·学习·程序人生·算法·生活
a1879272183111 小时前
【算法】动态规划第二篇:双序列 DP 三课——继承、计步与断链
算法·leetcode·动态规划·dp·回溯·暴力·算法讲解
江畔柳前堤12 小时前
前台·中台·后台:2026年AI原生时代的架构全景图
开发语言·人工智能·算法·机器学习·架构·scala·ai-native
文心快码BaiduComate13 小时前
2026 OPC x AI Coding 趋势案例交流会圆满落幕:一个人就一支研发团队
算法
ACM-moran13 小时前
条件显化(二分单峰函数+构造数列)
算法·推公式·整数三分