【力扣100题】52.最小路径和

题目描述

给定一个包含非负整数的 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 等多种变种。


相关推荐
快手技术2 小时前
将DSA注意力引入多模态,快手Keye2.0开启强化推理新范式
算法
圣保罗的大教堂2 小时前
leetcode 3043. 最长公共前缀的长度 中等
leetcode
码之气三段.2 小时前
牛客周赛 Round 145-E(写了200行的史山)
算法·深度优先
Hwang2522 小时前
Attention-04-decoder部分
算法
计算机安禾2 小时前
【算法分析与设计】第13篇:最小生成树:Prim算法与Kruskal算法的比较研究
大数据·人工智能·算法
vortex52 小时前
国密(商用密码)算法核心参数速查
算法·密码学
wuweijianlove3 小时前
算法中的记忆化思想与重复子问题优化的技术5
算法
小江的记录本4 小时前
【JVM虚拟机】垃圾回收GC:垃圾判定算法:引用计数法、可达性分析算法(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·spring·面试
Hello.Reader4 小时前
算法基础(十四)—— 随机化快速排序为什么平均表现很好
算法