动态规划:用“记住过去”解决复杂问题的智慧
动态规划(Dynamic Programming,DP)是一种通过记录子问题解来避免重复计算的算法思想。如果说贪心算法是“只看眼前最优”,那么动态规划就是“记住过去每一步的选择”。本文将通过生活化的例子和Java代码实现,带你轻松理解动态规划的精髓!
一、什么是动态规划?
动态规划的核心思想是:将大问题分解为小问题,通过保存小问题的解来避免重复计算。就像我们解数学题时先记住公式,下次遇到相同问题直接使用结果。
举个生活中的例子 🧩
假设你要爬10级台阶,每次可以爬1级或2级。问有多少种不同的方法?
如果用暴力递归会重复计算很多子问题(比如爬第8级台阶的方法会被反复计算)。而动态规划会用一个表格记录每一级台阶的方法数,后续直接查表。
二、动态规划的适用条件
动态规划需要满足两个关键条件:
- 最优子结构:大问题的最优解包含子问题的最优解
- 重叠子问题:子问题会被重复计算多次
⚠️ 注意:动态规划常用于解决贪心算法无法处理的复杂优化问题(如0-1背包问题)。
三、经典案例:硬币找零问题
问题描述
给定不同面额的硬币(如1元、3元、5元)和一个总金额(如11元),求组成该金额的最少硬币数。
(若面额为1、3、5时,贪心算法会失效:比如找9元,贪心选5+1+1+1=4枚,实际最优是3+3+3=3枚)
动态规划策略
- 定义dp数组:
dp[i]
表示金额i的最小硬币数 - 初始状态:
dp[0] = 0
- 状态转移方程:
dp[i] = min(dp[i-coin] + 1)
(对所有硬币面额)
Java代码实现
public class DpExample {
public static void main(String[] args) {
int[] coins = {1, 3, 5};
int amount = 11;
int[] dp = new int[amount+1];
Arrays.fill(dp, amount+1); // 初始化为极大值
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
System.out.println("最少需要硬币数: " +
(dp[amount] > amount ? -1 : dp[amount]));
}
}
输出结果
最少需要硬币数: 3 // 5+5+1(或3+3+5)
四、如何识别适用动态规划的问题?
遇到以下特征时,可以考虑动态规划:
- 问题可以分解为相似的子问题
- 子问题会被重复计算
- 常见应用场景:背包问题、最长公共子序列、编辑距离、股票买卖问题等
五、动态规划的两种实现方式
1. 自顶向下(记忆化搜索)
用递归实现,用哈希表缓存结果
适合子问题数量较少的情况
2. 自底向上(迭代填表)
用循环实现,显式构建dp表
适合需要优化空间复杂度的情况
六、动态规划的优缺点
✅ 优点
- 能保证得到全局最优解
- 通过存储中间结果大幅提升效率
❌ 缺点
- 需要额外存储空间
- 对无重叠子问题的问题反而降低效率
七、总结
动态规划像一本精心记录的笔记本:
- 拆解问题:将大象放进冰箱分三步
- 记住结果:避免重复造轮子
- 组合答案:用小问题的解构建大问题的解
下次遇到复杂问题时,尝试问自己:
“这个问题能否拆解成子问题?子问题是否会被重复计算?”
如果答案是肯定的,动态规划就是你需要的利器! 🔨
扩展思考:
尝试用动态规划解决「青蛙过河」问题(LeetCode 403),体会状态转移的设计技巧。对比暴力递归与动态规划的时间复杂度差异,感受DP的威力!