算法第三章作业

算法第三章作业:7-4 最低通行费 实践报告

  1. 实践报告

根据题意,商人从左上角走到右下角,且必须在 (2N-1) 的时间内穿出。这意味着在 N * N 的网格中,总步数刚好等于从左上到右下的最短步数,即商人只能向右或向下移动,不能走回头路。问题转化为求网格中从左上角到右下角的最小路径和。

1.1 递归方程式、定义与边界条件

状态定义:设 dp(i, j) 表示从当前坐标 (i, j) 出发,走到右下角终点 (N-1, N-1) 所需的最低通行费。
递归方程式:在当前格子 (i, j),商人只有两种选择:向下走 (i+1, j) 或向右走 (i, j+1)。为了让总费用最小,我们应当从这两个选择中挑一个费用更小的方向。因此状态转移方程为:

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

边界条件:

  1. 终点边界:当到达右下角终点时,无路可走,直接返回该格子的费用。即 if (i == N-1 && j == N-1) return a[i][j];
  2. 越界边界(巧用哨兵):当往下或往右走出网格时,代表此路不通。为了防止该方向的路径和影响到 min() 的判断,我们可以将越界返回值设为一个极大的数。在具体的代码实现中,我们在原数组外多开一行一列,并将其值设为 1000,当 if (i == N || j == N) 时直接返回 1000。

1.2 填表法的相关说明

由于记忆化递归是自顶向下的,当我们将它转化为等价的填表法时:

表的维度:需要一张二维表 dp[N][N]。
填表范围:行下标 i \in [0, N-1],列下标 j \in [0, N-1]。
填表顺序:因为当前状态 dp[i][j] 依赖于它的下方 dp[i+1][j] 和右方 dp[i][j+1](即自底向上推导),所以在填表时,必须从右下角往左上角逆向遍历。即外层循环 i 从 N-1 递减到 0,内层循环 j 从 N-1 递减到 0。
原问题最优值:填表法完成后,整张表的左上角元素 dp[0][0] 即为从起点到终点的最低通行费(等价于递归调用 dp(0, 0) 的返回值)。

1.3 时间与空间复杂度分析

时间复杂度:O(N^2)。
整个网格共有 N * N 个状态。在加入了记忆化数组(memo)后,每个状态只会被计算一次,每次状态转移只需 O(1) 的时间复杂度进行加法与 min 比较。因此总时间复杂度为 O(N^2)。
空间复杂度:O(N^2)。
除了存储 N * N 的原费用网格外,还需要一个 N * N 的记忆化数组(或 DP 数组)来保存计算过的状态。此外,递归调用还会占用系统栈空间,最坏情况(全走到底再拐弯)递归深度为 2N,即 O(N)。综合下来,空间复杂度主要由二维数组主导,为 O(N^2)。


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

通过第三章对动态规划(DP)的学习以及作业的练习,我对 DP 有了更深刻的认识:

  1. 核心在于状态与转移:动态规划不是某种具体的算法,而是一种解决问题的思想。最关键的是找到最优子结构,也就是大问题的最优解必须由小问题的最优解推导而来。无论是 最低通行费 里选下还是右,还是 0-1背包 里选不选当前物品,本质上都是在做多阶段决策。
  2. 记忆化搜索与填表法的等价性:我以前写题喜欢直接顺着思路写递归,但常常会面临超时问题。引入记忆化后,能让递归树剪枝,效率瞬间提升到和循环填表一样。自顶向下的记忆化代码逻辑清晰、符合人类直觉;而自底向上的填表法(二维 dp[i][j])则能更好地控制空间。
  3. 空间优化的魅力(滚动数组):在 0-1 背包问题中,我发现当当前行只依赖上一行时,其实没必要保留完整的二维表。通过一维数组加逆序循环的 滚动数组 技巧,可以将空间复杂度从 O(N * C) 降到 O(C),这让我体会到了算法设计中的极致与巧妙。
  4. 边界条件是生命线:不管是 最低通行费 的越界哨兵,还是 子集和 中 恰好凑成 j=0 返回 1 的特殊处理,边界条件写错一点,整个 DP 的推导都会崩盘。分析问题时必须严谨地把控边界。

动态规划最迷人的地方在于,它把一个原本可能需要指数级时间复杂度(暴力搜索)的问题,通过记录中间状态,硬生生地降维到了多项式时间(如 O(N^2)),这是算法思维里非常华丽的一步。

posted @ 2026-10-28 22:55  momo1号  阅读(9)  评论(0)    收藏  举报