动态规划 (DP) 是一种强大的算法范式,通过将复杂问题分解为更简单的子问题来解决。与其他问题解决方法不同,DP 会存储子问题的结果以避免重复计算,从而显著提高效率。本页面涵盖了动态规划的核心概念、特征以及常见的解决方法。
有关有时会与动态规划混淆的回溯算法,请参阅 回溯法。虽然这两种技术都涉及通过分解问题来解决问题,但它们在方法和应用上有所不同。
动态规划的特点是通过以下方式系统地解决问题优化:
动态规划的效率来自于避免了重新计算已经计算过的结果。当问题包含许多重叠的子问题时,这尤其强大。
来源:[docs/chapter_dynamic_programming/intro_to_dynamic_programming.md:1-15]
动态规划问题通常具备三个主要特征:
问题中需要多次解决同一个子问题。
在上图中,请注意 fib(3) 和 fib(2) 被计算了多次——这些是重叠子问题。
当一个问题的最优解可以从其子问题的最优解构造出来时,该问题就具有最优子结构。
例如,在最小爬楼梯问题中,到达第 i 阶台阶的最小成本是
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
这个方程直接从较小子问题的最优解构建最优解。
子问题的解仅取决于当前状态,而与如何达到该状态无关。此属性确保一旦我们解决了子问题,其解决方案将保持有效,无论未来的决定如何。
当问题由于额外的约束而失去此属性时,我们通常需要扩展状态定义来恢复它。
来源:[docs/chapter_dynamic_programming/dp_problem_features.md:1-101]
我们首先将问题递归地表达出来,将其分解为更小的自身版本。
由于重叠子问题的重复计算,这种方法会导致指数级的时间复杂度。
示例(爬楼梯问题)
来源:[docs/chapter_dynamic_programming/intro_to_dynamic_programming.md:21-53]
在记忆化方法中,我们维护一个已计算结果的缓存。
示例(带记忆化的爬楼梯)
来源:[docs/chapter_dynamic_programming/intro_to_dynamic_programming.md:59-74]
制表法从最小的子问题开始向上构建解决方案。
示例(带制表法的爬楼梯)
来源:[docs/chapter_dynamic_programming/intro_to_dynamic_programming.md:76-90]
许多 DP 问题只需要有限的先前状态窗口即可计算当前状态。这使得我们可以通过仅存储必要的状态来优化空间使用。
来源:[docs/chapter_dynamic_programming/intro_to_dynamic_programming.md:100-108]
在处理动态规划问题时,请遵循以下步骤:
通过检查以下几点来确定问题是否适合动态规划范式:
来源:[docs/chapter_dynamic_programming/dp_solution_pipeline.md:1-32]
在这个问题中,给定 n 个具有重量和价值的物品,我们需要在不超过重量限制的情况下最大化价值。
状态定义:dp[i][w] = 使用前 i 个物品,重量限制为 w 的最大价值
状态转移:如果 w >= wgt[i-1],则 dp[i][w] = max(dp[i-1][w], dp[i-1][w-wgt[i-1]] + val[i-1])
来源:[docs/chapter_dynamic_programming/knapsack_problem.md:1-36]
一种物品可以选择多次的变体。
状态转移:如果 w >= wgt[i-1],则 dp[i][w] = max(dp[i-1][w], dp[i][w-wgt[i-1]] + val[i-1])
与 0-1 背包的关键区别在于,选择一个物品后,您仍然可以选择同一个物品。
来源:[docs/chapter_dynamic_programming/unbounded_knapsack_problem.md:1-40]
给定一个每个单元格都有成本的网格,找到从左上角到右下角总成本最小的路径。
状态定义:dp[i][j] = 到达单元格 (i,j) 的最小成本
状态转移:dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
来源:[docs/chapter_dynamic_programming/dp_solution_pipeline.md:34-90]
衡量将一个字符串转换为另一个字符串所需的最少操作次数(插入、删除、替换)。
状态定义:dp[i][j] = 将字符串 1 的前 i 个字符转换为字符串 2 的前 j 个字符所需的最少编辑次数
状态转移
dp[i][j] = dp[i-1][j-1]dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])来源:[docs/chapter_dynamic_programming/edit_distance_problem.md:1-52]
找出找特定金额所需的最小硬币数。
状态定义:dp[i][a] = 使用前 i 种硬币找金额 a 所需的最少硬币数
状态转移:dp[i][a] = min(dp[i-1][a], dp[i][a-coins[i-1]] + 1)
计算使用给定硬币找特定金额的不同方法数。
状态定义:dp[i][a] = 使用前 i 种硬币找金额 a 的方法数
状态转移:dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]
来源:[docs/chapter_dynamic_programming/unbounded_knapsack_problem.md:70-206]
来源:[docs/chapter_dynamic_programming/summary.md:1-9]
通过观察我们只需要有限的先前状态窗口,许多 DP 问题都可以进行空间优化。
对于每个状态仅取决于前一行的二维 DP 问题。
示例转换
对于 0-1 背包:使用反向迭代以防止覆盖所需值。对于无界背包:使用正向迭代,因为我们希望重用更新后的值。
来源:[docs/chapter_dynamic_programming/knapsack_problem.md:135-168], [docs/chapter_dynamic_programming/unbounded_knapsack_problem.md:40-67]
来源:[docs/chapter_dynamic_programming/dp_problem_features.md:52-101]
大多数 DP 解决方案具有:
对于优化场景:
来源:[docs/chapter_dynamic_programming/summary.md:1-24]
刷新此 Wiki
最后索引时间2025年4月17日(d97611)