LeetCode中等难度的经典题目——64. 最小路径和,这道题是动态规划(DP)的入门必刷题,既能帮我们理解DP的核心思想,又能掌握网格类问题的解题套路。
一、题目解读:明确需求,找对方向
题目很简洁:给定一个 m x n 的网格,网格里全是非负整数,要求找出从左上角到右下角的路径,使得路径上的数字总和最小。还有一个关键限制——每次只能向下或者向右移动一步。
举个简单例子帮助理解:如果网格是 [[1,3,1],[1,5,1],[4,2,1]],那么最小路径就是 1→3→1→1→1,总和是7;或者 1→1→4→2→1,总和也是7(两种路径均可,核心是总和最小)。
这里要注意两个关键点:
-
移动方向限制:只能向下、向右,不能向上、向左,这决定了每个格子的“前序格子”只有两个(上方或左方);
-
非负整数:网格中没有负数,不用担心“绕路”能得到更小总和,只需专注于当前格子的最小路径推导。
二、解题思路:为什么用动态规划?
拿到这道题,首先会想到暴力解法——枚举所有从左上角到右下角的路径,计算每条路径的总和,再取最小值。但这样做的时间复杂度是 O(2^(m+n)),当 m 和 n 稍大(比如超过10),就会超时,显然不现实。
这时候就需要动态规划出场了。DP的核心思想是“重叠子问题”和“最优子结构”,咱们结合这道题拆解:
-
重叠子问题:从左上角到右下角的最小路径,依赖于“从左上角到当前格子的最小路径”,而多个格子的最小路径会重复计算同一个子路径(比如走到 (i,j) 和 (i+1,j),都可能用到 (i,j-1) 的路径和);
-
最优子结构:当前格子 (i,j) 的最小路径和,等于“上方格子 (i-1,j) 的最小路径和”和“左方格子 (i,j-1) 的最小路径和”中的较小值,再加上当前格子的值(因为只能从这两个方向过来)。
基于这个思路,我们可以用一个DP数组来存储每个格子的最小路径和,一步步推导到右下角,就能得到答案。
三、代码解析:逐行拆解,读懂每一步
先贴出完整代码(TypeScript版本,和题目给出的一致),再逐行拆解核心逻辑:
function minPathSum(grid: number[][]): number {
const m = grid.length;
const n = grid[0].length;
const dp = Array.from({ length: m }, () => new Array(n).fill(Infinity));
dp[0][0] = grid[0][0];
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i === 0 && j === 0) continue;
dp[i][j] = Math.min(
(j - 1) >= 0 ? dp[i][j - 1] : Infinity,
(i - 1) >= 0 ? dp[i - 1][j] : Infinity
) + grid[i][j];
}
}
return dp[m - 1][n - 1];
};
1. 初始化:定义DP数组及边界值
-
const m = grid.length:获取网格的行数 m; -
const n = grid[0].length:获取网格的列数 n(前提是网格非空,题目中默认grid有有效数据); -
const dp = Array.from({ length: m }, () => new Array(n).fill(Infinity)):创建一个和原网格大小一致的DP数组,初始值填充为无穷大(Infinity)。为什么用无穷大?因为我们要取“最小值”,初始化为无穷大,才能保证后续取min时,能正确拿到有效的前序路径和; -
dp[0][0] = grid[0][0]:边界条件——左上角格子(起点)的最小路径和,就是它本身的值(没有前序路径)。
2. 双重循环:遍历所有格子,推导DP值
用双重for循环遍历网格的每一个格子(i从0到m-1,j从0到n-1),逐个计算每个格子的最小路径和:
-
if (i === 0 && j === 0) continue:跳过起点(已经初始化过,无需重复计算); -
Math.min( (j - 1) >= 0 ? dp[i][j - 1] : Infinity, (i - 1) >= 0 ? dp[i - 1][j] : Infinity ):这是核心逻辑,计算“左方格子”和“上方格子”的最小路径和。这里要注意边界判断:-
当 j=0 时(第一列),没有左方格子,所以左方路径和设为无穷大(相当于只能从上方过来);
-
当 i=0 时(第一行),没有上方格子,所以上方路径和设为无穷大(相当于只能从左方过来);
-
-
+ grid[i][j]:当前格子的路径和,是前序最小路径和加上当前格子的值(因为要经过当前格子)。
3. 返回结果:右下角的DP值
return dp[m - 1][n - 1]:遍历完成后,DP数组右下角的数值,就是从左上角到右下角的最小路径和(因为我们已经推导完所有格子的最优路径)。
四、代码优化:空间复杂度降低(可选)
上面的代码空间复杂度是 O(m*n)(因为创建了一个m x n的DP数组),其实我们可以优化到 O(1),不需要额外创建DP数组,直接在原网格上修改即可。
思路:原网格的每个格子的值,替换成“到当前格子的最小路径和”,因为原网格的数值在计算完成后就不再需要(非负整数,不影响后续计算)。优化后的代码如下:
function minPathSum(grid: number[][]): number {
const m = grid.length;
const n = grid[0].length;
// 初始化第一行(只能从左向右走)
for (let j = 1; j < n; j++) {
grid[0][j] += grid[0][j - 1];
}
// 初始化第一列(只能从上向下走)
for (let i = 1; i < m; i++) {
grid[i][0] += grid[i - 1][0];
}
// 遍历剩余格子
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
}
}
return grid[m - 1][n - 1];
};
优化点说明:
-
第一行只能从左向右走,所以每个格子的路径和 = 前一个格子的路径和 + 当前格子值;
-
第一列只能从上向下走,所以每个格子的路径和 = 上一个格子的路径和 + 当前格子值;
-
剩余格子,直接用原网格的值累加左方或上方的最小值,无需额外空间。
五、常见易错点提醒
-
边界判断遗漏:忘记判断 j=0 或 i=0 的情况,导致取到 dp[i][j-1] 或 dp[i-1][j] 时出现数组越界;
-
DP数组初始化错误:没有将 dp[0][0] 设为 grid[0][0],或者初始值没有用无穷大,导致后续计算出现偏差;
-
循环顺序错误:必须从左上角到右下角遍历,不能反向(反向无法获取前序格子的最优路径和)。
六、总结:掌握网格类DP的通用套路
这道题虽然简单,但能帮我们总结出网格类DP的通用思路,以后遇到类似题目(比如“不同路径”“最小路径和变种”)都能套用:
-
定义DP数组:dp[i][j] 表示“从起点到 (i,j) 的最优解”(本题是最小路径和);
-
确定边界条件:起点的DP值,以及第一行、第一列的DP值(因为移动方向限制,这些格子的前序路径唯一);
-
推导状态转移方程:根据移动方向,确定当前格子的前序格子,进而写出DP值的计算方式(本题是 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]);
-
遍历网格,计算所有DP值,最终返回终点的DP值。