📌 题目链接:64. 最小路径和 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、动态规划、矩阵
⏱️ 目标时间复杂度:O(mn)
💾 空间复杂度:O(mn)(可优化至 O(n))
在 LeetCode Hot100 中,「最小路径和」 是一道非常典型的 二维动态规划(2D DP) 入门题。它不仅考察你对状态转移方程的理解,还隐含了 空间优化 的常见面试技巧。
无论你是准备校招、社招,还是想夯实算法基础,这道题都值得你深入掌握——因为它的思想会反复出现在更复杂的路径类问题中(如带障碍物的路径、最大路径和、概率路径等)。
🎯 题目分析
给定一个
m x n的非负整数网格grid,从左上角(0, 0)出发,每次只能 向右或向下 移动一步,求到达右下角(m-1, n-1)的路径数字总和最小值。
✅ 关键约束
- 只能 向右 或 向下 → 没有回头路,无环,天然适合 DP。
- 所有数字 ≥ 0 → 不存在“绕远路反而更小”的情况,贪心不可行(但 DP 可行)。
- 起点和终点固定 → 边界条件明确。
🧩 为什么用动态规划?
- 最优子结构:到达
(i, j)的最小路径和 = min(从上方来, 从左方来) + 当前值。 - 重叠子问题:多个路径会经过同一个格子,重复计算可被缓存。
- 无后效性:一旦确定
(i, j)的最小和,后续决策不受之前路径影响。
🧮 核心算法及代码讲解:二维动态规划(2D DP)
我们定义状态:
dp[i][j]表示 从 (0, 0) 到 (i, j) 的最小路径和。
📐 状态转移方程
根据移动规则,只有两种方式到达 (i, j):
- 从上方
(i-1, j)向下走 - 从左方
(i, j-1)向右走
因此:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
🧱 边界初始化
- 起点:
dp[0][0] = grid[0][0] - 第一行(只能从左来):
dp[0][j] = dp[0][j-1] + grid[0][j] - 第一列(只能从上来):
dp[i][0] = dp[i-1][0] + grid[i][0]
💡 面试加分点:空间优化
由于 dp[i][j] 只依赖 上一行 和 当前行左侧 的值,我们可以用 一维数组 滚动更新:
vector<int> dp(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] = min(dp[j], dp[j-1]) + grid[i][j];
}
}
✅ 空间复杂度从 O(mn) 降至 O(n),是高频面试优化点!
🧭 解题思路(分步拆解)
- 处理边界情况:若网格为空,直接返回 0。
- 初始化 DP 表:创建与
grid同尺寸的dp二维数组。 - 填充第一行和第一列:因为路径唯一,直接累加。
- 遍历其余格子:按行优先顺序,利用状态转移方程填表。
- 返回结果:
dp[m-1][n-1]即为答案。
🔄 遍历顺序很重要!必须保证在计算
dp[i][j]时,dp[i-1][j]和dp[i][j-1]已经计算完成。因此采用 从左到右、从上到下 的顺序。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(m × n):每个格子访问一次 |
| 空间复杂度 | O(m × n)(标准解法);可优化为 O(n)(滚动数组) |
| 是否可原地修改? | ✅ 可以!直接复用 grid 作为 DP 表(若允许修改输入) |
| 扩展性 | 支持添加障碍物(设为 INF)、求路径数量、打印路径等 |
💬 面试常问:
- “如果网格中有障碍物(-1 表示),怎么改?”
- “如何输出具体路径?”
- “能否用 BFS 或 DFS?为什么不用?”
⚠️ 注意:DFS/BFS 在此题中会超时(指数级路径数),而 DP 是多项式时间,效率碾压。
💻 代码实现
✅ C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size(), columns = grid[0].size();
// 创建 dp 表,大小与 grid 一致
auto dp = vector<vector<int>>(rows, vector<int>(columns));
// 起点初始化
dp[0][0] = grid[0][0];
// 初始化第一列:只能从上方来
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 初始化第一行:只能从左方来
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 填充其余格子:取上方和左方的最小值 + 当前值
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
// 返回右下角的最小路径和
return dp[rows - 1][columns - 1];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<vector<int>> grid1 = {{1,3,1},{1,5,1},{4,2,1}};
cout << "Test 1: " << sol.minPathSum(grid1) << "\n"; // 输出: 7
// 测试用例 2
vector<vector<int>> grid2 = {{1,2,3},{4,5,6}};
cout << "Test 2: " << sol.minPathSum(grid2) << "\n"; // 输出: 12
return 0;
}
✅ JavaScript(等效实现)
/**
* @param {number[][]} grid
* @return {number}
*/
var minPathSum = function(grid) {
if (grid.length === 0 || grid[0].length === 0) return 0;
const rows = grid.length;
const cols = grid[0].length;
const dp = Array.from({ length: rows }, () => Array(cols).fill(0));
dp[0][0] = grid[0][0];
// 初始化第一列
for (let i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 初始化第一行
for (let j = 1; j < cols; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 填充其余部分
for (let i = 1; i < rows; i++) {
for (let j = 1; j < cols; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][cols - 1];
};
// 测试
console.log(minPathSum([[1,3,1],[1,5,1],[4,2,1]])); // 7
console.log(minPathSum([[1,2,3],[4,5,6]])); // 12
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!