引言
动态规划 (Dynamic Programming, DP) 是算法设计中的一种极其强大的思想,它将一个复杂问题分解为一系列更小、可管理的、且相互重叠的子问题。通过系统地求解这些子问题并存储其结果,DP 能够避免冗余计算,从而在处理具有特定结构性质(最优子结构和重叠子问题)的问题时,展现出惊人的效率。
对于许多开发者而言,DP 似乎是一座难以逾越的高山。其核心难点在于如何将一个具体问题抽象成数学模型。然而,一旦你掌握了其内在的思维范式——定义状态与推导状态转移方程——你会发现 DP 是一门充满逻辑之美的艺术。
第一章:动态规划的精髓
在投身于具体问题之前,我们必须深刻理解 DP 的两大支柱及其解决问题的通用方法论。
1.1 DP 的适用前提
一个问题若能用动态规划求解,通常必须满足两个核心性质:
- 最优子结构 (Optimal Substructure)
- 定义:一个问题的最优解包含其子问题的最优解。这意味着我们可以放心地从子问题的最优解出发,构建出原问题的最优解。
- 直观理解:假设你找到了到达目的地的最短路径,那么这条路径上的任意一段子路径,也必然是其两个端点之间的最短路径。如果不是,我们就可以用更短的子路径替换它,从而得到一条比原'最短路径'更短的路径,这与前提矛盾。0-1 背包和 LCS 问题都具备此性质。
- 重叠子问题 (Overlapping Subproblems)
- 定义:在递归求解的过程中,许多相同的子问题会被反复计算。
- 直观理解:以斐波那契数列为例,计算
fib(5)需要fib(4)和fib(3);而计算fib(4)又需要fib(3)和fib(2)。可以看到,fib(3)被计算了多次。DP 的核心优势正是利用'备忘录'(通常是一个数组或哈希表)来存储这些子问题的解,当再次遇到时直接查表,将递归树的指数级时间复杂度'剪枝'为多项式级别。
1.2 解决 DP 问题的通用五步法
面对一个看似复杂的 DP 问题,遵循一个结构化的方法论至关重要。这五个步骤将成为你分析问题的得力工具:
- 定义状态 (State Definition):这是最关键、最具创造性的一步。你需要明确
dp数组(或函数)的含义。dp[i]或dp[i][j]究竟代表了什么?一个好的状态定义应该能够清晰地描述子问题的解,并且无后效性(即当前状态的决策不受未来状态的影响)。 - 推导状态转移方程 (State Transition Equation):这是算法的核心。它定义了问题状态之间如何演进。你需要思考
dp[i]是如何由dp[i-1],dp[i-2], … 等一个或多个已知的子问题状态推导出来的。 - 初始化 (Initialization):确定
dp数组的初始值或边界条件。这些初始值是状态转移方程的起点。错误的初始化会导致整个递推过程的失败。 - 确定遍历顺序 (Traversal Order):根据状态转移方程,确定
dp数组的填充顺序。必须保证在计算dp[i]时,其所依赖的所有子问题状态(如dp[i-1])都已经计算完毕。 - 返回结果 (Return Result):根据
dp数组的定义,确定问题的最终解在dp数组的哪个位置。
接下来,我们将运用这套方法论,彻底征服两大经典模型。
第二章:经典模型一:0-1 背包问题
2.1. 问题描述
给定
n个物品,每个物品有其重量 和价值 。现在有一个容量为 的背包。你需要决定如何选择物品放入背包,才能在不超过背包总容量的前提下,使得所选物品的总价值最大。注意:每个物品只有一件,要么选,要么不选,不可分割。


