第三章 动态规划

实践报告:7-4 最低通行费

1.1 递归方程式

状态定义:设 dp[i][j] 表示商人从左上角 (1,1) 走到格子 (i,j) 时,已经缴纳的最小总费用(包含 (i,j) 本身的费用)。

状态转移方程:

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

其中 a[i][j] 是题目给定的、经过格子 (i,j) 需要缴纳的费用。这个方程的依据是:到达 (i,j) 的最后一步,要么是从上方 (i-1,j) 下来的,要么是从左方 (i,j-1) 过来的。在“只能向右或向下走”的前提下,没有第三种可能,所以取两个前驱状态中费用较小的那个,再加上当前格子的费用即可。

边界条件:

· dp[1][1] = a[1][1]:起点没有前驱,费用就是它本身的费用。
· 第一行(i = 1, j > 1):dp[1][j] = dp[1][j-1] + a[1][j]。第一行的格子只能从左边走过来,没有“上方”可选。
· 第一列(j = 1, i > 1):dp[i][1] = dp[i-1][1] + a[i][1]。第一列的格子只能从上边下来,没有“左方”可选。

这里有一个实际写代码时容易踩的坑:如果数组从 0 开始存、并且把整个 dp 表默认初始化为 0,那么在求最小值的语境下,第 0 行和第 0 列那些“不存在的位置”会以费用 0 的身份被错误地参与 min 比较,导致第一行或第一列的结果被压低。稳妥的做法是把第一行和第一列单独手动累加,或者把边界外侧初始化为一个足够大的数,让它们永远不可能被选为最小值。

1.2 填表法

表的维度:二维表,大小为 N × N,dp[i][j] 对应网格中第 i 行第 j 列的位置。

填表范围:i 从 1 到 N,j 从 1 到 N。整个表都要填,因为最终答案需要用到右下角的值,而每一个格子的值都只依赖于它的上方和左方,所以不存在“某些格子可以不填”的情况。

填表顺序:按行从上到下,每行内从左到右。理由很直接:dp[i][j] 依赖 dp[i-1][j](上一行同列,已经填过)和 dp[i][j-1](本行前一列,刚刚填过)。按这个顺序走一遍,每个格子被计算时,它需要的两个值都已经准备好了,不需要回溯或重复访问。

原问题的最优值:表格元素 dp[N][N],也就是走到右下角那个格子的最小累计费用。

1.3 时间和空间复杂度

时间复杂度:O(N^2)。填表过程本质上是把 N \times N 个格子各访问一次,每个格子的转移只做了一次加法和一次比较,是常数时间操作。第一行和第一列的初始化各花 O(N),被 O(N^2) 的主体部分吸收,所以总量级仍然是 O(N^2)。

空间复杂度:O(N^2),需要一个和原网格同规模的二维 dp 表。严格来说,如果只求最终费用而不需要回溯路径,可以用一维滚动数组把空间压到 O(N):因为 dp[i][j] 只依赖“上一行同列”和“本行前一列”两个值,遍历到第 i 行时,第 i-2 行及更早的数据已经不会再被用到了。但按照填表法的标准写法,不做这个优化时,空间就是 O(N^2)。

对动态规划算法的理解和体会

做这道题之前,我对动态规划最直接的印象是“把大问题拆成小问题,把算过的存起来”。这个说法本身没错,但放在这道题上,如果只记住这句话,大概率会卡在两个地方。

第一个卡点是 2N-1 这个时间限制。一开始很容易把它当成一个额外的“优化目标”,去想有没有什么走法能在 2N-1 步之内“尽量省费用”。但实际上,N×N 网格从左上到右下,最短路径的长度恰好就是 2N-1 步(起点算一步的话是 2N-1,不算起点是 2N-2 步)。题目说“必须在 2N-1 个单位时间穿越出去”,意思是在步数上没有任何余量——一旦向左或向上走一步,就必然超出限制。所以这个条件的真正作用不是“目标”,而是约束的翻译:它把“上下左右都能走”的模糊局面,一刀切成了“只能向右或向下”。想通这一点之后,后面的状态定义和转移方程几乎是自然浮现的。我后来觉得,动态规划题里最花时间的往往不是写递推式,而是把题面里一句看似普通的话,翻译成状态空间上的一个硬性剪枝。

第二个卡点是边界的“最小值陷阱”。求最大值的时候,数组默认初始化为 0 问题不大,因为 0 本来就不可能被最大值选中;但求最小值时,默认的 0 会变成一个“非法但极具竞争力”的候选者。如果不把第一行和第一列手动处理好,dp[1][2] 会去比较 dp[0][2] = 0,然后得到一个明显偏小的错误结果。这让我意识到,动态规划里“边界条件”不是走过场的仪式,而是状态转移方程在定义域边缘处能不能成立的关键:边界处理错了,中间填得再规整也是错的。

还有一点体会是关于“状态定义”和“代码实现”的关系。这道题的代码其实很短,核心循环就那几行,但如果 dp[i][j] 的含义没有想清楚——“是从起点到该点的最小费用,还是从该点出发到终点的最小费用?包不包含当前格子的费用?”——写出来的递推式就会在某个地方差一个 a[i][j]。动态规划的难度集中在动手之前:把状态的含义钉死,把转移的方向理顺,把边界确认一遍。这三件事做扎实了,填表本身反而是最机械的部分。

posted @ 2026-10-27 09:02  TheWorld_23  阅读(13)  评论(0)    收藏  举报