文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
核心思路:二维网格动态规划
- 状态定义
设dp[i][j]表示从起点(0, 0)到达网格位置(i, j)的最小路径和。 - 状态转移方程
因为每次只能向下或向右移动,所以到达(i, j)的前一步只可能来自上方(i-1, j)或左侧(i, j-1): d p i j = min ( d p i − 1 j , d p i j − 1 ) + grid i j dpij = \min(dpi - 1j, dpij - 1) + \text{grid}ij dpij=min(dpi−1j,dpij−1)+gridij - 边界条件
- 起点: d p 0 0 = grid 0 0 dp00 = \text{grid}00 dp00=grid00
- 第一行(只能向右走): d p 0 j = d p 0 j − 1 + grid 0 j dp0j = dp0j - 1 + \text{grid}0j dp0j=dp0j−1+grid0j
- 第一列(只能向下走): d p i 0 = d p i − 1 0 + grid i 0 dpi0 = dpi - 10 + \text{grid}i0 dpi0=dpi−10+gridi0
- 空间优化思路
- 原地修改( O ( 1 ) O(1) O(1) 额外空间):如果不限制修改原数组,可以直接把原矩阵
grid当作dp数组使用。 - 一维滚动数组( O ( n ) O(n) O(n) 额外空间):如果要求不破坏输入数据,由于每一行的状态只依赖于当前行的上一列和上一行的同列,维护一个长度为 n n n 的一维数组即可完成递推。
- 原地修改( O ( 1 ) O(1) O(1) 额外空间):如果不限制修改原数组,可以直接把原矩阵
2、解题代码
cpp
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
// 1. 初始化第一行:只能从左侧向右走,路径和为前缀和
for (int j = 1; j < n; j++) {
grid[0][j] += grid[0][j - 1];
}
// 2. 初始化第一列:只能从上方往下走,路径和为前缀和
for (int i = 1; i < m; i++) {
grid[i][0] += grid[i - 1][0];
}
// 3. 状态转移:到达 (i, j) 只能来自上方 (i - 1, j) 或左方 (i, j - 1)
// 选择两者中路径和较小的一个,并在原数组上原地更新(空间复杂度 O(1))
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
}
}
// 右下角即为从左上角到达终点的最小路径和
return grid[m - 1][n - 1];
}
};
复杂度分析
- 时间复杂度: O ( m × n ) O(m \times n) O(m×n),遍历一次网格的所有单元格。
- 空间复杂度: O ( 1 ) O(1) O(1),直接在原数组上操作,无额外空间开销。
三、知识风暴
动态规划(Dynamic Programming) 是本题的核心算法思想。它通过将原问题拆解为若干重叠子问题,并利用「最优子结构」性质,用子问题的最优解递推得到全局最优解。对于「最小路径和」这类最优化型动态规划问题,动态规划能以 O ( m × n ) O(m \times n) O(m×n) 的复杂度高效求解。
算法核心思想:
- 最优子结构 :到达位置 ( i , j ) (i, j) (i,j) 的最小路径和,可以由「上方格子 ( i − 1 , j ) (i - 1, j) (i−1,j) 的最小路径和」与「左方格子 ( i , j − 1 ) (i, j - 1) (i,j−1) 的最小路径和」中的较小者,加上当前格子的代价 grid i j \text{grid}ij gridij 得到。只要子问题 d p i − 1 j dpi - 1j dpi−1j、 d p i j − 1 dpij - 1 dpij−1 已知,就能递推得到当前位置的最优解。
- 重叠子问题 :在递推过程中,同一个格子 ( i , j ) (i, j) (i,j) 的最优解会被多个后续格子反复引用。例如计算 d p i + 1 j dpi + 1j dpi+1j 与 d p i j + 1 dpij + 1 dpij+1 时,都会访问 d p i j dpij dpij 的状态,因此用滚动数组缓存结果可避免重复计算。
- 与贪心的区别:贪心每一步只做当前最优选择、不回溯;而动态规划会枚举「向右」与「向下」两种可能的来源方向,取其中路径和更小者,从而保证结果的正确性。
常见对比:动态规划 vs 贪心
- 动态规划 :时间复杂度 O ( m × n ) O(m \times n) O(m×n),空间复杂度 O ( n ) O(n) O(n)(滚动数组优化后)。适合需要同时考虑「向右/向下」两种决策、且局部最优不能直接决定全局最优的场景,通用性更强。
- 贪心算法 :时间复杂度 O ( m + n ) O(m + n) O(m+n),空间复杂度 O ( 1 ) O(1) O(1)。适合每一步的局部最优能直接推导全局最优的场景,代码简洁高效,但本题中路径选择无法用贪心直接证明(例如到达某个格子时,贪心只选当前「路径和更小」的方向,就可能错过最终最优解)。
- 共同点:两者都依赖「最优子结构」性质。区别在于贪心只保留一个当前最优状态,而动态规划需要同时维护「向右」与「向下」两种来源的状态。
动态规划的设计思想:
- 核心思想:把大问题拆成小问题,先解决小问题,再用小问题的答案拼出大问题的答案。本题中,先求出第一行与第一列所有格子的前缀和,再逐个格子递推出后续位置的状态。
- 与本题的联系 :最小路径和问题天然具有递推结构------每个格子 ( i , j ) (i, j) (i,j) 的最优解都可以由「上方格子 ( i − 1 , j ) (i - 1, j) (i−1,j) 的最小路径和」与「左方格子 ( i , j − 1 ) (i, j - 1) (i,j−1) 的最小路径和」中的较小者加上当前格子的代价得到。因此无需回溯或搜索,只需按顺序滚动更新即可。
- 注意事项 :动态规划的正确性依赖于「最优子结构」与「无后效性」。本题中 d p i j dpij dpij 只由上方与左方两个格子的状态决定,与未来的状态无关,因此递推顺序合法。
使用要点:
- 状态变量 :
dp[j]记录到达当前第 j j j 列的最小路径和( 0 ≤ j < n 0 \le j < n 0≤j<n),用于累计从起点到当前行的最小代价。 - 初始化 :
dp[j]初始化为第一行的前缀和(第一行只能一直向右走,路径和为累加值),以「第一行」作为递推基准。 - 转移时机 :外层循环遍历每一行 i i i,内层循环从第 1 1 1 列正序遍历到第 n − 1 n - 1 n−1 列,执行
dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]。正序保证左侧累加后的值能立即参与当前行的后续计算。 - 结果返回 :遍历结束后,返回
dp[n - 1],表示到达右下角 ( m − 1 , n − 1 ) (m - 1, n - 1) (m−1,n−1) 的最小路径和。
算法变体与扩展:
- 不同路径(LeetCode 62):将「求最小和」改为「计数」,状态转移方程由取最小值改为求和,与本题的递推结构高度相似。
- 带障碍物的最小路径和(LeetCode 63 变体):在网格中引入障碍物,遇到障碍物时该格路径和置为无穷大(不可达),思路与本题一脉相承。
- 三角形最小路径和(LeetCode 120):多阶段动态规划的经典题目,通过维护每层的最短路径状态来刻画不同阶段的约束。
- 不同路径 III(LeetCode 980):在网格中引入「必须经过所有空格」的约束,需要结合回溯与状态压缩,是本题的进阶扩展。
相关 LeetCode 例题:
- 62. 不同路径(网格 + 计数)
- 63. 不同路径 II(带障碍物 + 计数)
- 120. 三角形最小路径和(多阶段 + 最小值)
- 980. 不同路径 III(回溯 + 计数)

