1. 题目
给定一个包含非负整数的 m × n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
1 2 3 4 5 6 7
| 输入: grid = [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 路径 1→3→1→1→1 总和最小 = 7。
|
2. 解题思路
只能向右或向下 ⇒ 到达 (i, j) 的前驱只有 (i-1, j) 和 (i, j-1),是最经典的 DP。
2.1 状态定义
dp[i][j] = 从左上角到 (i, j) 的最小路径和。
转移方程:
1
| dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
|
边界:第一行只能从左边来 dp[0][j] = dp[0][j-1] + grid[0][j];第一列只能从上边来 dp[i][0] = dp[i-1][0] + grid[i][0]。
- 时间复杂度:O(m·n),空间复杂度:O(m·n)。
2.2 空间优化到 O(n)
每行只依赖「上一行」和「本行左边」,可用一维滚动数组:dp[j] = grid[i][j] + min(dp[j](上一行的值), dp[j-1](本行左边的值))。
极限可原地改 grid 到 O(1)(若允许修改输入)。
3. TypeScript 实现
3.1 二维 DP(清晰)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function minPathSum(grid: number[][]): number { const m = grid.length; const n = grid[0].length; const dp = Array.from({ length: m }, () => new Array(n).fill(0));
dp[0][0] = grid[0][0]; for (let j = 1; j < n; j++) dp[0][j] = dp[0][j - 1] + grid[0][j]; for (let i = 1; i < m; i++) dp[i][0] = dp[i - 1][0] + grid[i][0];
for (let i = 1; i < m; i++) { for (let j = 1; j < n; j++) { dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]); } }
return dp[m - 1][n - 1]; }
console.log(minPathSum([[1, 3, 1], [1, 5, 1], [4, 2, 1]]));
|
3.2 一维滚动数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function minPathSumOpt(grid: number[][]): number { const m = grid.length; const n = grid[0].length; const dp = new Array(n).fill(0);
for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { if (i === 0 && j === 0) dp[j] = grid[0][0]; else if (i === 0) dp[j] = dp[j - 1] + grid[i][j]; else if (j === 0) dp[j] = dp[j] + grid[i][j]; else dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i][j]; } }
return dp[n - 1]; }
|
4. 面试延伸
- 变体:不同路径(计数而非求和)把
min 换成 +,是同一网格 DP 骨架。
- 带障碍 / 三角形最小路径和 / 地下城游戏:注意「地下城游戏」需从右下倒推(HP 约束),说明并非所有网格 DP 都能正向做。
- 追问:为什么一维数组里
dp[j] 在更新前代表上一行?因为本行还没覆盖它,天然滚动——这是滚动数组的核心技巧。
难度:中等 | LeetCode 64 | 网格 DP 入门