【LeetCode Hot100 刷题日记 (92/100)】64. 最小路径和 —— 数组、矩阵、动态规划(DP)经典二维路径问题🧠

0 阅读5分钟

📌 题目链接: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),是高频面试优化点!


🧭 解题思路(分步拆解)

  1. 处理边界情况:若网格为空,直接返回 0。
  2. 初始化 DP 表:创建与 grid 同尺寸的 dp 二维数组。
  3. 填充第一行和第一列:因为路径唯一,直接累加。
  4. 遍历其余格子:按行优先顺序,利用状态转移方程填表。
  5. 返回结果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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!