好的,我注意到你已看过大量题目。我将为你讲解一个未被列出的线性动态规划经典题目。
题目:最小路径和(Minimum Path Sum)
题目描述:
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角 (0, 0) 到右下角 (m-1, n-1) 的路径,使得路径上的数字总和为最小。
规则:每次只能向下或者向右移动一步。
示例:
输入:
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1]
]
输出:7
解释:因为路径 1 → 3 → 1 → 1 → 1 的总和最小,为 7。
解题思路循序渐进讲解
这是一个典型的二维线性动态规划问题。我们的目标是找到从起点到终点的最小累计代价。
核心思路: 由于只能向右或向下走,到达网格中任意一个点 (i, j) 的最小路径和,只可能来源于它正上方的格子 (i-1, j) 或者它正左方的格子 (i, j-1),再加上当前格子自身的值。我们用一个二维 DP 数组 dp[i][j] 来存储这个“最小路径和”。
步骤 1:定义状态
定义 dp[i][j] 表示:从起点 (0, 0) 走到格子 (i, j) 的最小路径和。
我们的最终答案就是 dp[m-1][n-1]。
步骤 2:建立状态转移方程
思考如何到达 (i, j):
- 如果从上方
(i-1, j)下来,那么路径和为dp[i-1][j] + grid[i][j]。 - 如果从左方
(i, j-1)过来,那么路径和为dp[i][j-1] + grid[i][j]。
为了得到最小和,我们取这两种来源的最小值。
因此,状态转移方程为:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
步骤 3:确定初始条件(Base Cases)
初始条件是动态规划的起点,必须单独处理。
- 起点
(0, 0): 到达起点的最小路径和就是它自身的值。所以dp[0][0] = grid[0][0]。 - 第一行
(i = 0): 对于第一行的任何格子(0, j),它只能从其左边的格子(0, j-1)向右移动到达。因此,dp[0][j] = dp[0][j-1] + grid[0][j]。 - 第一列
(j = 0): 对于第一列的任何格子(i, 0),它只能从其上面的格子(i-1, 0)向下移动到达。因此,dp[i][0] = dp[i-1][0] + grid[i][0]。
步骤 4:计算顺序
我们需要计算 dp[i][j],必须先知道 dp[i-1][j] 和 dp[i][j-1]。
最自然的计算顺序是:从左到右,从上到下,一行一行(或一列一列)地遍历整个网格。
这样,当计算到 (i, j) 时,它上方和左方的状态都已经计算好了。
步骤 5:算法实现(以示例为例,逐步演算)
我们用示例网格来手动演算一遍 dp 表的填充过程。
初始网格 grid:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]
-
初始化
dp[0][0]:dp[0][0] = grid[0][0] = 1。dp: [1, ?, ?] [?, ?, ?] [?, ?, ?] -
填充第一行
(i=0):dp[0][1] = dp[0][0] + grid[0][1] = 1 + 3 = 4dp[0][2] = dp[0][1] + grid[0][2] = 4 + 1 = 5
dp: [1, 4, 5] [?, ?, ?] [?, ?, ?] -
填充第一列
(j=0):dp[1][0] = dp[0][0] + grid[1][0] = 1 + 1 = 2dp[2][0] = dp[1][0] + grid[2][0] = 2 + 4 = 6
dp: [1, 4, 5] [2, ?, ?] [6, ?, ?] -
填充内部格子
(i=1, j=1):dp[1][1] = min(dp[0][1], dp[1][0]) + grid[1][1] = min(4, 2) + 5 = 2 + 5 = 7
dp: [1, 4, 5] [2, 7, ?] [6, ?, ?] -
填充内部格子
(i=1, j=2):dp[1][2] = min(dp[0][2], dp[1][1]) + grid[1][2] = min(5, 7) + 1 = 5 + 1 = 6
dp: [1, 4, 5] [2, 7, 6] [6, ?, ?] -
填充内部格子
(i=2, j=1):dp[2][1] = min(dp[1][1], dp[2][0]) + grid[2][1] = min(7, 6) + 2 = 6 + 2 = 8
dp: [1, 4, 5] [2, 7, 6] [6, 8, ?] -
填充终点
(i=2, j=2):dp[2][2] = min(dp[1][2], dp[2][1]) + grid[2][2] = min(6, 8) + 1 = 6 + 1 = 7
dp: [1, 4, 5] [2, 7, 6] [6, 8, 7]
最终答案 dp[2][2] = 7,与示例一致。
步骤 6:空间优化(进阶思考)
观察状态转移方程 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j],计算当前行 dp[i][...] 时,只需要用到上一行 dp[i-1][...] 和当前行已经计算过的左邻居 dp[i][j-1]。
因此,我们可以将二维 dp 数组优化为一维数组 dp[j],其长度为 n(列数):
dp[j]在更新前,存储的是上一行第j列的值(即旧的dp[i-1][j])。dp[j-1]在更新时,存储的是当前行第j-1列已经更新过的值(即新的dp[i][j-1])。
优化后的状态转移变为:
dp[j] = min(dp[j](上一行的值), dp[j-1](当前行左边的值)) + grid[i][j]
同时,需要小心处理第一行和第一列的初始化。
# 空间优化后的Python代码示例
def minPathSum(grid):
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
dp = [0] * n
# 初始化第一行
dp[0] = grid[0][0]
for j in range(1, n):
dp[j] = dp[j-1] + grid[0][j]
# 遍历剩余行
for i in range(1, m):
# 每行开始时,更新第一列(只能从上方来)
dp[0] = dp[0] + grid[i][0]
for j in range(1, n):
# dp[j] 是上一行j列的值,dp[j-1]是本行已更新的j-1列的值
dp[j] = min(dp[j], dp[j-1]) + grid[i][j]
return dp[-1]
总结:
“最小路径和”问题通过定义清晰的状态 dp[i][j],利用只能向右/向下的移动规则,推导出简洁的状态转移方程。正确处理边界条件(第一行、第一列)后,按顺序计算即可。这个问题是理解二维网格上动态规划的绝佳入门,其空间优化技巧也值得掌握。