Hot 100 --- 最小路径和
本文概览:本文讲解最小路径和的核心思路:只能向下或向右移动时,到达每个格子的最小路径和有且仅有上方和左方两个来源取较小值,用动态规划递推;与不同路径同结构,差别只在状态含义与初始化,方法二用一维数组滚动到 O(n) 空间
一、题目

二、题目分析
1. 题目要求
给定一个包含非负整数的 m × n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。返回这个最小路径和。
示例:grid = [[1,3,1],[1,5,1],[4,2,1]] → 7
路径
1 → 3 → 1 → 1 → 1总和为 7。
2. 怎么想这题?
和上一题《不同路径》是同一张网:只能向下或向右,所以到达任意一个格子 (i, j),只有两个来源——从正上方 (i-1, j) 下来一步,或从正左方 (i, j-1) 走过来一步,没有第三种。
区别在这题要的是"数字总和最小"。那站在 (i, j) 往回看:不管从哪条路来,想让它到这儿的累计和最小,就选"(i-1, j) 的最小路径和"和"(i, j-1) 的最小路径和"里较小的那个,再加上 grid[i][j] 本身。(i, j) 只关心到达它上面和左边两个格子各自的最优值,不用管它们具体怎么走的——这又是动态规划的"无后效性"。
和上一题的区别只在一个地方:状态存的是什么、初始化怎么填。上一题存"路径条数",边界是"第一行第一列全 1";这题存"最小路径和",边界来自"第一行只能从左来、第一列只能从上走",不能随便填 1。
3. 需要解决哪几个问题?
问题一:状态怎么定义?dp[i][j] 表示什么?
问题二:转移方程怎么写?到达 (i, j) 的最小和,由哪几个已知值推出来?
问题三:第一行和第一列怎么初始化?这两处的来源只有一个,能不能直接套转移方程?
三、方法一:二维 DP 数组,O(m × n) 空间
1. 思路概览
1 | public int minPathSum(int[][] grid) { |
思路简要说明:
- 状态定义:
dp[i][j]表示从(0,0)走到(i,j)的最小路径和 - 转移方程:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j],选上方或左方较小的那个,再加上自身 - 初始化:
dp[0][0] = grid[0][0];第一行是累加前缀(只能从左来)、第一列是累加前缀(只能从上走) - 遍历顺序:按行从上到下、每行从左到右,保证算
(i,j)时上方和左方都已求出 - **时间复杂度 O(m × n)**,空间 O(m × n)
2. 思路详解
第一步:解决状态定义——dp[i][j] 表示什么?
题目要的是"从左上角到右下角的最小路径和"。照样把状态安在每个格子上:
dp[i][j] = 从 (0,0) 走到格子 (i,j) 的最小数字总和。
答案就是 dp[m-1][n-1]。
第二步:解决转移方程——为什么取 min 还要加自身?
这两个来源对应两条真实的三路,走任何一条到 (i,j):从上方来,累计和是 dp[i-1][j],再踩上 grid[i][j];从左方来,累计和是 dp[i][j-1],再踩上 grid[i][j]。要最小,两个里挑小的那把:
1 | dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] |
注意和上一题不同:上一题是 +(路径条数相加),这题是 min(挑一条路)+ grid[i][j]。因为每个格子自身的数字必须算进总和里。
第三步:解决边界——第一行、第一列为什么不能用转移方程?
转移方程要读 dp[i-1][j] 和 dp[i][j-1] 两个值再取 min。但:
- 第一行(
i = 0):没有上方格子dp[-1][j],它只能从一个来源来——从左边的格子"向右"走。所以dp[0][j]得单独累加:dp[0][j] = dp[0][j-1] + grid[0][j],一路把前缀和加起来。 - 第一列(
j = 0):没有左方格子dp[i][-1],只能从一个来源来——从上边的格子"向下"走。所以dp[i][0] = dp[i-1][0] + grid[i][0]。 - 起点(
(0,0)):不走也有它自己这一格,dp[0][0] = grid[0][0]。
如果 (0,0) 也套转移方程,会读到两个不存在的格子;而第一行、第一列若搞一个统一的初始值,就丢了"只能沿一条边往下/往右累加"这个事实。所以边界必须单独初始化,而不是像上题那样全填 1。
这也就是题目分析里"问题三"的答案:第一行、第一列来源唯一,必须先独立累加初始化。
第四步:完整执行过程
以示例网格为例:
1 | grid: |
3. 复杂度分析
**时间复杂度 O(m × n)**:每个格子常数次运算。
**空间复杂度 O(m × n)**:完整的二维 dp 表。
四、能不能也用一维数组省空间?
和上题一样,转移方程 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] 只依赖两个值:正上方的 dp[i-1][j] 和正左方的 dp[i][j-1]。算完一行,上一行就没用了,两行可以挤进同一个一维数组。上一题滚动靠 row[j] += row[j-1](一条路够了不必取 min),这一题要取 min,滚动写法关键的差别在于每一行的 row[0] 不能继续用旧的 1,得先自主更新。
五、方法二:一维数组滚动,O(n) 空间
1. 思路概览
1 | public int minPathSum(int[][] grid) { |
思路简要说明:
- 状态定义:
row[j]表示当前行第j列的最小路径和,随行滚动更新 - 初始化:先用第一行填满
row(只能从左累加) - **每行开头更新
row[0]**:只能从上一行同列累加,row[0] += grid[i][0] - 内部递推:
row[j] = min(row[j](上方旧值), row[j-1](本行新值)) + grid[i][j] - 返回:
row[n-1] - **时间复杂度 O(m × n)**,空间 O(n)
2. 思路详解
第一步:初始化为什么先填第一行?
一维 row 对应"当前行"。循环从 i = 1 开始,进入前 row 得先装好第一行。第一行只能从左累加,于是:
1 | row[j] = row[j-1] + grid[0][j] |
这行代码和方法一里 dp[0][j] 的初始化一模一样。
第二步:为什么每一行的开头要先更新 row[0]?
滚动数组的坑在这。进入第 i 行时,row[0] 还留着上一行第 0 列的值(上一轮算出来的)。但第一列(j = 0)只能从上方走来,所以第 i 行的 row[0] 应该等于"上一行的 row[0] + grid[i][0]"。
如果不更新,row[0] 就卡在旧的第一行值上,后面算 row[1] 要用它当"左方值"就错了。所以每进一行先执行:
1 | row[0] += grid[i][0] |
第三步:row[j] = min(row[j], row[j-1]) + grid[i][j] 在算啥?
进入第 i 行的内层循环时,row 里同时躺着两层信息:
row[j]此刻还是上一行同列dp[i-1][j]的最小路径和,也就是"上方"那份;row[j-1]已经在本轮改成本行 j-1 列的新值,也就是"左方"那份。
取二者较小,再踩上当前格子的 grid[i][j],写回 row[j]——把"上方"和"左方"两条路compare后选小,正好等价于方法一的转移方程。覆盖后 row[j] 只剩本行的新值,数组始终只保留一行。
对比上一题:上一题 l22 row[j] += row[j-1] 是直接把左方值加到上方值上;这题因为各自格子数字要参与、还要取 min,写成 Math.min(row[j], row[j-1]) + grid[i][j]。其他完全相同。
第四步:完整执行过程
1 | grid: |
每一轮的 row,正好对应方法一二维表里那一行的取值,滚到最后一行,row[n-1] 就是答案。
3. 复杂度分析
**时间复杂度 O(m × n)**:两层循环规模不变。
**空间复杂度 O(n)**:只留一行,从 O(m × n) 降到 O(n)。
六、总结
| 维度 | 不同路径(上一题) | 最小路径和(本题) |
|---|---|---|
| 状态含义 | 路径条数 | 最小路径和 |
| 转移 | dp[i][j] = dp[i-1][j] + dp[i][j-1](求和) |
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j](取小+自身) |
| 第一行/第一列 | 全填 1 | 按"只能往右/只能往下"独立累加前缀和 |
| 一维滚动 | row[j] += row[j-1] |
每行开头先 row[0] += grid[i][0],再 row[j] = min(row[j], row[j-1]) + grid[i][j] |
两道题共用同一套思路骨架:只能向下或向右,决定了到达每个格子只有上方、左方两个来源。差别全在两个字——上一题问"有几条",于是状态是条数、转移是求和、边界全 1;这题问"最小和是多少",于是状态是最小和、转移是取 min 再叠加自身、边界是"来源唯一所以必须独立累加"。
如果一开始就直接套上一题的模板把第一行第一列全填 1,会犯一个典型错误:dp[0][0] 凭空被赋成 1,丢掉 grid[0][0],后面所有格子都被污染。先想清楚"每个位置的最小和只能由哪个来源得到",再定初始化,就顺了。






