动态规划
- 如何判断一个问题是不是动态规划问题?
- 求解动态规划问题该从何处入手,完整步骤是什么?
如何判断动态规划问题?
[!NOTE] DP三大特征
如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。然而,我们很难从问题描述中直接提取出这些特性。因此我们通常会放宽条件,具体如下
- 先验条件:是否能用回溯(决策树)、DFS 暴力穷举?
- 加分项:
- 问题目标是最优解(最大值/最小值)。
- 状态转移可被公式化(如
dp[i] = dp[i-1] + ...
)。
- 减分项:
- 需要输出所有解(而非最优解)。动态规划通常会在求解过程中”压缩”或”丢弃”次优解,只保留最优解用于后续计算,保存所有解需要指数级空间
- 明显属于排列组合问题(例如全排列)。
通过分析这三个特征,可以系统化判断一个问题是否适合动态规划。
动态规划的三个核心特征是重叠子问题、最优子结构和无后效性。以下通过具体例子和通俗解释说明它们的含义和区别:
1. 重叠子问题(Overlapping Subproblems)
- 定义:在求解过程中,不同的问题会重复调用相同的子问题。动态规划通过缓存子问题的结果避免重复计算,提高效率。
- 示例:计算斐波那契数列
fib(n) = fib(n-1) + fib(n-2)
: - 递归树中存在大量重复节点(比如
fib(3)
被多次计算)。- 动态规划通过直接查表跳过重复计算。
2. 最优子结构(Optimal Substructure)
定义:问题的最优解可以通过其子问题的最优解组合得到。必须保证通过子问题的最优解能直接推导出原问题的最优解。如果最后结果可能从次优子结构产生,那么则不适合DP,尤其是全局最优解不一定包含子问题的最优解的那些问题
示例:最短路径问题(DP):
- 如果从节点 A 到 C 的最短路径是
A → B → C
,那么A → B
的路径也必须是 A 到 B 的最短路径(否则整体不是最短的)。 - 反例:最长路径问题。
例如,在环形图中,A → B → C → A
可以无限循环,无法直接通过子问题的解推导出全局最优解。
- 如果从节点 A 到 C 的最短路径是
3. 无后效性(Non-aftereffect)
- 定义:某一阶段的状态一旦确定,后续的决策不会影响之前的状态(未来的决策仅依赖于当前状态)。
- 示例:经典的背包问题:
- 状态
dp[i][w]
表示前i
个物品在背包容量w
下的最大价值。 - 选择物品时,不管之前的物品如何放入(只关心当前剩余的容量),后续决策不会改变已选择的物品。
- 反例:带有状态的依赖问题。
例如,股票买卖问题中,如果交易次数会影响后续操作(如交易费),则不符合无后效性(需通过多维状态描述)。
- 状态
4. 状态压缩(State Compression) 是动态规划中一种优化空间复杂度的技巧/思想。其核心思想是:
只保留对当前决策有影响的部分状态,丢弃无关或不再使用的历史状态。
三者的关系
特性 | 核心作用 | 示例问题 | 反例 |
---|---|---|---|
重叠子问题 | 通过缓存子问题结果减少计算量 | 斐波那契数列 | 快速排序(无重叠) |
最优子结构 | 保证全局最优解由局部最优解组合得到 | 最短路径、背包问题 | 最长路径问题 |
无后效性 | 确保未来决策与历史无关,简化状态转移方程 | 背包问题 | 有状态依赖的问题、需要维护具体路径或排列组合的方案,而DP通常只关心最优值,不关心具体路径 |
注意:路径只要是求一个最短路径,也可能是动态规划,许多动态规划问题可以通过逆向推导状态转移路径来恢复具体的解。这种方法通常被称为 “路径回溯” 或 **”解重构”**。
记忆化搜索 VS 动态规划
记忆化搜索和动态规划是解决重叠子问题的两种方法,后者在前者不管不顾的基础上,优化了空间复杂度,显式循环解决了递归,它们有以下主要区别:
- 实现方向
- 记忆化搜索:自顶向下 (Top-down),从原问题出发,通过递归调用解决子问题
- 动态规划:自底向上 (Bottom-up),从最小的子问题开始,逐步构建出原问题的解
- 状态转移方式
- 记忆化搜索:通过函数的递归调用隐式地表达状态转移
- 动态规划:显式地写出状态转移方程,通常使用循环实现
- 空间利用
- 记忆化搜索:需要额外的递归调用栈空间
- 动态规划:一般只需要 DP 数组空间,有时还可以进行空间优化
- 代码实现对比
记忆化搜索示例:
1 | def fib(n, memo={}): |
动态规划示例:
1 | def fib(n): |
- 适用场景
记忆化搜索:
- 状态转移比较复杂的问题
- 边界情况较多
- 不是所有状态都需要计算
- 需要快速实现时
动态规划:
- 状态转移规律明显
- 需要求解所有状态
- 对空间复杂度要求高
- 需要优化性能时
- 性能比较
- 记忆化搜索:可能会有函数调用开销,但只计算需要的状态
- 动态规划:没有递归开销,但会计算所有可能的状态
选择建议:
- 如果问题的状态转移复杂,用记忆化搜索更容易实现
- 如果需要优化性能和空间,选择动态规划
- 在实际工程中,可以先用记忆化搜索实现,后续需要优化时再转换为动态规划
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Min的博客!
评论