64. 最小路径和
❓ 问题简介
给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明: 每次只能向下或者向右移动一步。
示例说明
📌 示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
📌 示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
💡 解题思路
这是一个典型的**动态规划(Dynamic Programming)**问题。我们目标是从左上角 (0, 0) 到右下角 (m-1, n-1) 找出路径和最小的路径。
✅ 思路一:二维 DP(推荐)
步骤分解:
-
定义状态
设
dp[i][j]表示从(0, 0)到(i, j)的最小路径和。 -
状态转移方程
因为只能从上方或左方过来:
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]
- 第一行只能从左边来:
-
初始化
dp[0][0] = grid[0][0] -
结果
返回
dp[m-1][n-1]
✅ 思路二:空间优化的一维 DP
观察发现,计算当前行时只依赖上一行和当前行左侧的值,因此可以用一维数组优化空间。
- 使用长度为
n的数组dp - 更新顺序:从左到右
- 转移方程:
- 对于第一行:
dp[j] = dp[j-1] + grid[0][j] - 对于其他行:
dp[0] += grid[i][0](第一列)dp[j] = grid[i][j] + min(dp[j], dp[j-1])
- 对于第一行:
❌ 其他不推荐方法
- DFS / BFS 暴力搜索:时间复杂度过高(指数级),会超时。
- 记忆化递归:本质上等价于 DP,但常数较大,不如迭代清晰。
💻 代码实现
java
// 方法一:二维 DP
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
// 初始化起点
dp[0][0] = grid[0][0];
// 初始化第一行
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 初始化第一列
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 填充其余位置
for (int i = 1; i < m; i++) {
for (int 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];
}
}
// 方法二:一维 DP(空间优化)
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[] dp = new int[n];
dp[0] = grid[0][0];
// 初始化第一行
for (int j = 1; j < n; j++) {
dp[j] = dp[j - 1] + grid[0][j];
}
// 从第二行开始更新
for (int i = 1; i < m; i++) {
dp[0] += grid[i][0]; // 第一列只能从上边来
for (int j = 1; j < n; j++) {
dp[j] = grid[i][j] + Math.min(dp[j], dp[j - 1]);
}
}
return dp[n - 1];
}
}
go
// 方法一:二维 DP
func minPathSum(grid [][]int) int {
m, n := len(grid), len(grid[0])
dp := make([][]int, m)
for i := range dp {
dp[i] = make([]int, n)
}
dp[0][0] = grid[0][0]
// 初始化第一行
for j := 1; j < n; j++ {
dp[0][j] = dp[0][j-1] + grid[0][j]
}
// 初始化第一列
for i := 1; i < m; i++ {
dp[i][0] = dp[i-1][0] + grid[i][0]
}
// 填充其余位置
for i := 1; i < m; i++ {
for j := 1; j < n; j++ {
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
}
}
return dp[m-1][n-1]
}
// 方法二:一维 DP(空间优化)
func minPathSum(grid [][]int) int {
m, n := len(grid), len(grid[0])
dp := make([]int, n)
dp[0] = grid[0][0]
// 初始化第一行
for j := 1; j < n; j++ {
dp[j] = dp[j-1] + grid[0][j]
}
// 从第二行开始更新
for i := 1; i < m; i++ {
dp[0] += grid[i][0] // 第一列只能从上边来
for j := 1; j < n; j++ {
dp[j] = grid[i][j] + min(dp[j], dp[j-1])
}
}
return dp[n-1]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
💡 注意:Go 语言中需要手动实现
min函数(Go 1.21+ 可用slices.Min,但此处为通用性自定义)。
🧪 示例演示
以 grid = [[1,3,1],[1,5,1],[4,2,1]] 为例:
二维 DP 表构建过程:
| 0 | 1 | 2 | |
|---|---|---|---|
| 0 | 1 | 4 | 5 |
| 1 | 2 | 7 | 6 |
| 2 | 6 | 8 | 7 |
dp[0][0] = 1dp[0][1] = 1+3=4,dp[0][2]=4+1=5dp[1][0] = 1+1=2dp[1][1] = 5 + min(2,4) = 7dp[1][2] = 1 + min(7,5) = 6dp[2][0] = 2+4=6dp[2][1] = 2 + min(6,7) = 8dp[2][2] = 1 + min(8,6) = 7✅
最终答案:7
✅ 答案有效性证明
我们可以用数学归纳法证明 DP 的正确性:
- 基础情况 :
dp[0][0] = grid[0][0]显然正确。 - 归纳假设 :假设对于所有
(i', j')满足i' < i或j' < j,dp[i'][j']已是最小路径和。 - 归纳步骤 :到达
(i, j)只能从(i-1, j)或(i, j-1)来,根据归纳假设,这两个位置的值已是最优,因此取较小者加上grid[i][j]即为(i, j)的最优解。
因此,DP 状态转移正确,最终结果有效。
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 二维 DP | O(m × n) | O(m × n) |
| 一维 DP(优化) | O(m × n) | O(n) |
- 时间 :每个格子访问一次,共
m × n次。 - 空间:二维 DP 需要额外二维数组;一维 DP 只需一行空间,可原地优化(若允许修改原数组,甚至可 O(1) 空间)。
💡 实际面试中,建议先写二维 DP 清晰表达思路,再提出空间优化方案。
🔚 问题总结
- ✅ 核心思想:动态规划,利用子问题最优解构造全局最优解。
- ✅ 关键观察:只能向右或向下 → 当前状态仅依赖上方和左方。
- ✅ 优化方向:从二维 DP 到一维 DP,空间从 O(mn) 降至 O(n)。
- ✅ 适用场景:网格路径、最小/最大代价类问题(如不同路径、三角形最小路径和等)。
📌 延伸思考:
- 如果允许向四个方向移动?→ 需用 Dijkstra(带权最短路)。
- 如果有障碍物?→ 在 DP 中跳过障碍位置即可(如 LeetCode 63)。
掌握此类 DP 模板,可快速解决大量网格路径优化问题!
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions