64. 最小路径和(Minimum Path Sum)

难度:中等 | 主题:动态规划、矩阵

题目

给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和最小。每次只能向下或者向右移动一步。

示例

 
grid = [[1,3,1],
 
        [1,5,1],
 
        [4,2,1]]
 
输出:7
 
解释:1 → 3 → 1 → 1 → 1
 

思路

先讲个故事:放学回家有两条路

你家住在网格的左上角,学校在右下角。你每天放学回家只能向右或向下走,每个路口有个”堵车指数”(格子里的数字)。

你想找一条总堵车指数最小的路线。

这就像导航 App 里的路径规划——只是简化版:只有两个方向,每个格子有代价,求最小代价路径


引导式推导:从加法到取 min

第 1 步(暴力递归):到 (i,j) 的最小路径和 = 当前格子值 + min(从左边来, 从上边来)。

 
到 (i,j) 的最小路径和 = grid[i][j] + min(到 (i-1,j) 的最小路径和,
 
                                           到 (i,j-1) 的最小路径和)
 

但暴力递归画一下递归树就会发现重复计算严重——和斐波那契一样。

第 2 步(发现子问题重叠):到 (i,j) 的最小路径和只依赖上方和左方的结果。而且路径具有无后效性——不管之前怎么走到 (i,j) 的,到 (i,j) 之后的选择只依赖当前位置,不依赖之前的路径。

这就是 DP 的核心条件:最优子结构 + 无后效性

第 3 步(自底向上填表):定义 dp[i][j] = 到达 (i,j) 的最小路径和。

 
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
 

graph TD

    subgraph 网格路径规划

        A["起点 (0,0)<br/>dp[0][0]=1"] -->|"向右"| B["(0,1)<br/>dp=1+3=4"]

        A -->|"向下"| C["(1,0)<br/>dp=1+1=2"]

        B -->|"向右"| D["(0,2)<br/>dp=4+1=5"]

        B -->|"向下"| E["(1,1)<br/>dp=min(2,4)+5=7"]

        C -->|"向右"| E

        C -->|"向下"| F["(2,0)<br/>dp=2+4=6"]

        D -->|"向下"| G["(1,2)<br/>dp=min(7,5)+1=6"]

        E -->|"向右"| G

        E -->|"向下"| H["(2,1)<br/>dp=min(6,7)+2=8"]

        F -->|"向右"| H

        G -->|"向下"| I["终点 (2,2)<br/>dp=min(8,6)+1=7 ✅"]

        H -->|"向右"| I

    end

    style I fill:#ffd700,stroke:#333


三层递进:从暴力到一维滚动


graph LR

    subgraph 三层递进

        A["暴力递归<br/>O(2^(m+n))"]

        B["二维 DP<br/>O(mn) O(mn)"]

        C["一维滚动<br/>O(mn) O(n)"]

    end

    A -->|发现重叠子问题| B

    B -->|发现只依赖左和上| C

暴力递归:从起点到终点,每次选 min。画递归树会发现大量重复路径计算。

二维 DPdp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])。先初始化第一行(只能从左来)和第一列(只能从上来),然后按行遍历填表。

一维滚动dp[j] 在更新前是上一行的值(上方),dp[j-1] 当前行已更新(左方)。所以一行代码搞定:

 
dp[j] = grid[i][j] + min(dp[j], dp[j-1])
 
#       ↑上方    ↑左方(已更新)
 

和 70爬楼梯 的滚动变量逻辑完全一样——只留有用的,砍掉不需要的历史


代码

 
def minPathSum(self, grid):
 
    m, n = len(grid), len(grid[0])
 
    dp = [0] * n                     # 一维滚动数组,长度 = 列数
 
    for i in range(m):               # 逐行遍历
 
        for j in range(n):           # 逐列遍历
 
            if i == 0 and j == 0:    # 起点
 
                dp[j] = grid[0][0]
 
            elif i == 0:             # 第一行:只能从左来
 
                dp[j] = dp[j - 1] + grid[i][j]
 
            elif j == 0:             # 第一列:只能从上来
 
                dp[j] = dp[j] + grid[i][j]   # dp[j] 此时是上一行的值
 
            else:                    # 普通位置:取 min(上方, 左方)
 
                dp[j] = grid[i][j] + min(dp[j], dp[j - 1])
 
    return dp[n - 1]                 # 右下角的最小路径和
 

空间优化核心dp[j] 在更新前存储的是上一行同列的值(即”上方”),dp[j-1] 在当前行已更新(即”左方”)。


复杂度

指标解释
时间O(mn)遍历整个矩阵一次
空间O(n)一维滚动数组,n 为列数
二维 DPO(mn)可提但非最优

实战考量

频率分析

出现在:美团/字节/阿里 二面 DP 题,约 25% 的同类题会考矩阵 DP。常作为 62不同路径 的进阶——从计数变求 min,考察对 DP 状态转移的灵活运用。

延伸思考

Q:和 62 题(不同路径)的关系?

A:框架完全一样——都是 dp[i][j] = f(dp[i-1][j], dp[i][j-1])。62 是计数(+),63 是有障碍(跳过障碍格子),64 是求最小路径和(min)。会一个等于会三个

Q:空间能优化到 O(1) 吗?

A:可以——直接在原数组 grid 上原地修改。但会改变输入数据,实践中需和确认是否允许。

Q:如果要求输出具体路径呢?

A:额外用一个 dir 数组记录每个位置从哪来(上/左),最后从终点回溯到起点。这叫记录决策的 DP,在很多问题中都用到。

Q:如果还可以向右上走呢?

A:状态转移要多加一个右上来源 dp[i-1][j+1],就不能只用一行滚动了——滚动方向冲突。需要两行缓存或二维数组。

Q:如果格子值有负数呢?

A:状态转移公式不变,但要注意初始化——dp[j] 初始值不能是 0(因为负路径和可能小于 0),需要初始化为无穷大或特殊处理。

易错点

  • 第一行和第一列的初始化:第一行只能从左来,第一列只能从上来

  • 空间优化后 dp[j] 的语义变化——更新前是上方,更新后是当前位置

  • dp[j-1] 在当前行已更新,代表左方;dp[j] 未更新,代表上方——两个值的来源不同


生活类比

最小路径和 → 导航规划 → 记账本

想象你在记账本上填数字:每个格子的最小路径和 = 格子本身的数字 + 左边和上边两个格子中更小的那个。

这就像写导航 App 的路径规划——每个路口的预估时间 = 当前路口耗时 + 左边和上面两个入口中最快的。

滚动数组就是:只看上一行的账本,当前行的边算边写,算完上一行就扔掉。

还是那四个字——只留有用的。


相关题目

题目关系
62不同路径路径计数版,同类框架
63不同路径II有障碍物,跳过障碍格子
64最小路径和本题,路径加权求最小
70爬楼梯一维 DP 滚动优化同源

→ 返回题单:LeetCode学习路线图 > 十三、动态规划

速记卡(面试闪卡)

Q1:一句话讲清「64. 最小路径和(Minimum Path Sum)」到底是什么?

A:网格左上到右下、只能右/下走,求数字和最小路径——DP:每格 = 自身 + min(上, 左),可滚动到 O(n)。

Q2:题目本质 —— 怎么理解?

A:你家住网格左上、学校右下,只能右或下走,每个路口有「堵车指数」,找总指数最小路线。像导航 App 路径规划简化版:只有两方向、每格有代价、求最小代价路径。英语 Minimum Path Sum。

Q3:思路:最优子结构 + 无后效性 —— 怎么理解?

A:到 (i,j) 最小和 = grid[i][j] + min(到上方, 到左方)。因为路径无后效性——不管怎么走到这,之后的选择只依赖当前位置。这就是 DP 两条件:最优子结构 + 无后效性。自底向上填表即可。

Q4:空间优化:一维滚动 —— 怎么理解?

A:二维 dp[i][j] = grid + min(dp[i-1][j], dp[i][j-1])。一维滚动时 dp[j] 更新前是「上方」、dp[j-1] 已更新是「左方」,一行搞定。和爬楼梯滚动同源——只留有用的,砍掉历史。

Q5:复杂度与变体 —— 怎么理解?

A:时间 O(mn)(遍历矩阵),空间 O(n)(一维滚动);可原地改 grid 到 O(1) 但改输入慎用。和 62 不同路径同框架(一个 min、一个 +),63 加障碍跳过,会一个等于会三个。

Q6:核心速记主线有哪些?

  • 每格最小和 = 自身 + min(上方, 左方)

  • 依赖最优子结构 + 无后效性

  • 一维滚动:dp[j] 更新前是上方、dp[j-1] 是左方

  • 时间 O(mn)、空间 O(n);62/63 同框架

口诀

A:网格找最小和

只留上方和左方

滚动数组一行算

最小路径不迷茫

相关链接