题目描述
给定一个包含非负整数的 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
解释:路径 1 -> 2 -> 3 -> 6 或 1 -> 4 -> 5 -> 6
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|---|
| 暴力搜索 | 递归尝试所有路径,记录最小和 | O(2^(m+n)) | O(m+n) | 会超时 |
| 记忆化搜索 | 递归 + 备忘录,避免重复计算 | O(m*n) | O(m*n) | 剪枝优化 |
| 动态规划 | 自底向上填表 | O(m*n) | O(m*n) | 最常用 |
| 空间优化 | 一维数组滚动 | O(m*n) | O(n) | 面试最优 |
一、动态规划(最常用)
思路
定义 dp[i][j] = 从 (0,0) 到 (i,j) 的最小路径和。
状态转移方程:
- 机器人只能从上方 (i-1,j) 或左方 (i,j-1) 到达 (i,j)
- 取两者中路径和较小的那个,加上当前格子的值
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
边界条件:
- 第一列
dp[i][0] = dp[i-1][0] + grid[i][0](只能从上往下走) - 第一行
dp[0][j] = dp[0][j-1] + grid[0][j](只能从左往右走)
完整代码
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
// 定义二维 dp 数组
vector<vector<int>> dp(m, vector<int>(n, 0));
// 原点初始化
dp[0][0] = grid[0][0];
// 初始化第一列:只能从上往下走
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][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++) {
for (int 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];
}
};
二、算法流程图
以 grid = [[1,3,1],[1,5,1],[4,2,1]] 为例(3行3列):
原始网格:
j:0 1 2
+---+---+---+
i:0 | 1 | 3 | 1 |
+---+---+---+
i:1 | 1 | 5 | 1 |
+---+---+---+
i:2 | 4 | 2 | 1 |
+---+---+---+
Step 1: 初始化 dp 数组(全部置 0)
j:0 1 2
+---+---+---+
i:0 | 0 | 0 | 0 |
+---+---+---+
i:1 | 0 | 0 | 0 |
+---+---+---+
i:2 | 0 | 0 | 0 |
+---+---+---+
Step 2: 原点 dp[0][0] = grid[0][0] = 1
j:0 1 2
+---+---+---+
i:0 | 1 | 0 | 0 |
+---+---+---+
i:1 | 0 | 0 | 0 |
+---+---+---+
i:2 | 0 | 0 | 0 |
+---+---+---+
Step 3: 初始化第一列 dp[i][0] = dp[i-1][0] + grid[i][0]
dp[1][0] = dp[0][0] + grid[1][0] = 1 + 1 = 2
dp[2][0] = dp[1][0] + grid[2][0] = 2 + 4 = 6
j:0 1 2
+---+---+---+
i:0 | 1 | 0 | 0 |
+---+---+---+
i:1 | 2 | 0 | 0 |
+---+---+---+
i:2 | 6 | 0 | 0 |
+---+---+---+
Step 4: 初始化第一行 dp[0][j] = dp[0][j-1] + grid[0][j]
dp[0][1] = dp[0][0] + grid[0][1] = 1 + 3 = 4
dp[0][2] = dp[0][1] + grid[0][2] = 4 + 1 = 5
j:0 1 2
+---+---+---+
i:0 | 1 | 4 | 5 |
+---+---+---+
i:1 | 2 | 0 | 0 |
+---+---+---+
i:2 | 6 | 0 | 0 |
+---+---+---+
Step 5: 填表 dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
dp[1][1] = grid[1][1] + min(dp[0][1], dp[1][0]) = 5 + min(4, 2) = 7
dp[1][2] = grid[1][2] + min(dp[0][2], dp[1][1]) = 1 + min(5, 7) = 6
dp[2][1] = grid[2][1] + min(dp[1][1], dp[2][0]) = 2 + min(7, 6) = 8
dp[2][2] = grid[2][2] + min(dp[1][2], dp[2][1]) = 1 + min(6, 8) = 7
最终 dp:
j:0 1 2
+---+---+---+
i:0 | 1 | 4 | 5 |
+---+---+---+
i:1 | 2 | 7 | 6 |
+---+---+---+
i:2 | 6 | 8 | 7 |
+---+---+---+
答案:dp[2][2] = 7 OK
路径追溯:1 -> 3 -> 1 -> 1 -> 1 = 7
三、空间优化(面试最优解)
思路
第 i 行只依赖第 i-1 行,可以用一个一维数组完成。
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<int> dp(n, 0);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) {
dp[j] = grid[i][j]; // 原点
} 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] = grid[i][j] + min(dp[j], dp[j - 1]); // 取较小者
}
}
}
return dp[n - 1];
}
};
图解空间优化
以 grid = [[1,3,1],[1,5,1],[4,2,1]] 为例:
初始化 dp = [0, 0, 0]
处理 i=0(第一行):
j=0: dp[0] = 1 -> [1, 0, 0]
j=1: dp[1] = dp[0] + 3 = 1 + 3 = 4 -> [1, 4, 0]
j=2: dp[2] = dp[1] + 1 = 4 + 1 = 5 -> [1, 4, 5]
处理 i=1(第二行):
j=0: dp[0] = dp[0] + 1 = 1 + 1 = 2 -> [2, 4, 5]
j=1: dp[1] = 5 + min(4, 2) = 5 + 2 = 7 -> [2, 7, 5]
j=2: dp[2] = 1 + min(5, 7) = 1 + 5 = 6 -> [2, 7, 6]
处理 i=2(第三行):
j=0: dp[0] = dp[0] + 4 = 2 + 4 = 6 -> [6, 7, 6]
j=1: dp[1] = 2 + min(7, 6) = 2 + 6 = 8 -> [6, 8, 6]
j=2: dp[2] = 1 + min(6, 8) = 1 + 6 = 7 -> [6, 8, 7]
答案:dp[2] = 7 OK
时间复杂度:O(m*n)
空间复杂度:O(n)
四、进一步优化:原地修改 grid
思路
既然 dp 只依赖 grid,可以直接在 grid 上修改,不额外占用空间。
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) continue; // 原点,跳过
if (i == 0) {
// 第一行,只能从左来
grid[i][j] += grid[i][j - 1];
} else if (j == 0) {
// 第一列,只能从上来
grid[i][j] += grid[i - 1][j];
} else {
// 取上方和左方的较小者
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
}
}
}
return grid[m - 1][n - 1];
}
};
空间复杂度:O(1)(仅用了常数个变量)
五、记忆化搜索(递归版)
思路
从终点往回递归,每个格子选择来自上方或左方的较小路径和。
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> memo(m, vector<int>(n, -1));
return dfs(m - 1, n - 1, grid, memo);
}
private:
int dfs(int i, int j, vector<vector<int>>& grid, vector<vector<int>>& memo) {
// 到达原点
if (i == 0 && j == 0) return grid[0][0];
// 之前计算过
if (memo[i][j] != -1) return memo[i][j];
int ans = 0;
if (i == 0) {
// 在第一行,只能从左来
ans = dfs(i, j - 1, grid, memo) + grid[i][j];
} else if (j == 0) {
// 在第一列,只能从上来
ans = dfs(i - 1, j, grid, memo) + grid[i][j];
} else {
// 取上方和左方的较小者
ans = grid[i][j] + min(dfs(i - 1, j, grid, memo), dfs(i, j - 1, grid, memo));
}
memo[i][j] = ans;
return ans;
}
};
六、逐行解析(对照原题代码)
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(); // 行数
int n = grid[0].size(); // 列数
// 定义二维 dp 数组,m 行 n 列
vector<vector<int>> dp(m, vector<int>(n, 0));
// 原点:最小路径和就是 grid[0][0] 本身
dp[0][0] = grid[0][0];
// 初始化第一列:只能从上往下走
// dp[i][0] = dp[i-1][0] + grid[i][0]
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 初始化第一行:只能从左往右走
// dp[0][j] = dp[0][j-1] + grid[0][j]
for (int i = 1; i < n; i++) { // 注意:原题变量名用的是 i,实际是列索引
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
// 填表:对于其他位置,取上方和左方的较小值
for (int i = 1; i < m; i++) {
for (int 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];
}
};
七、与第62题对比
| 维度 | 第62题 不同路径 | 第64题 最小路径和 |
|---|---|---|
| 目标 | 计数(多少条) | 求和(最小和) |
| 初始化 | dp[i][0] = 1, dp[0][j] = 1 | dp[i][0] += grid[i][0], dp[0][j] += grid[0][j] |
| 转移方程 | dp[i][j] = dp[i-1][j] + dp[i][j-1] | dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) |
| 边界 | 第一行/列为 1 | 第一行/列为累计和 |
复杂度分析总结
| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|
| 暴力搜索 | O(2^(m+n)) | O(m+n) | 会超时 |
| 记忆化搜索 | O(m*n) | O(m*n) | 剪枝优化 |
| 动态规划 | O(m*n) | O(m*n) | 最常用 |
| 空间优化 | O(m*n) | O(n) | 面试最优 |
| 原地修改 | O(m*n) | O(1) | 最优空间 |
面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q: 为什么用 min 而不是加法? | 因为题目要求最小路径和,所以每次选择使路径和更小的方向 |
| Q: 如果网格中有负数怎么办? | 同样用 DP,负数会被正确处理(因为 min 会选择更小的负数路径) |
| Q: 空间还能更优化吗? | 可以原地修改 grid,空间降到 O(1),但会修改输入数据 |
| Q: 能否用 BFS? | BFS 可以,但需要维护最小堆(或每步都更新),复杂度高,不推荐 |
| Q: 如何输出最小路径? | 除了 dp 数组,再用一个 path 数组记录每步的选择方向,最后回溯 |
相关题目
| 题目编号 | 题目名称 | 难度 | 核心差异 |
|---|---|---|---|
| 62 | 不同路径 | 中等 | 计数问题,无权重 |
| 63 | 不同路径 II | 中等 | 有障碍物 |
| 64 | 最小路径和 | 中等 | 带权重的最小路径和 |
| 120 | 三角形最小路径和 | 中等 | 三角形网格,从顶到底 |
| 174 | 地下城游戏 | 困难 | 从右下向左上 DP,求最小生命值 |
| 931 | 下降路径最小和 | 中等 | 矩阵,可向左下/正下/右下走 |
总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 动态规划,每个位置由上方或左方转移而来 |
| 状态定义 | dp[i][j] = 从 (0,0) 到 (i,j) 的最小路径和 |
| 转移方程 | dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]) |
| 初始化 | 原点 dp[0][0] = grid[0][0],第一行/列是累计和 |
| 空间优化 | 一维数组 O(n) 或原地修改 O(1) |
| 与62题对比 | 计数变求和,+1 变 +min |
这道题是第62题(不同路径)的进阶版,将"计数"变为"求最小和"。掌握后可以拓展到带障碍物、三角形网格、从终点往回 DP 等多种变种。