文章目录
1 动态规划题目类型
2 动态规划解题步骤(例题:Coin Change)
Coin Change问题描述:
- 你有三种硬币(2、5、7),每种数量无穷多
- 你去书店买一本27元的书,老板无零钱,你需要刚好付27元
- 你支付的硬币个数最少是多少个?
注意:由于问题中涉及“最少”,因此考虑使用动态规划求解
2.1 确定状态
2.1.1 最后一步
假设最优策略:a1, a2, …, ak
最后一步就是对ak的决策(取2/ 5/ 7)
因此,a1, a2, …, ak-1拼出了27 - ak,这也是最优策略
为什么也是最优策略?
反证法:
- 若仅用k - 2枚硬币即可拼出27 - ak(即a1, a2, …, ak-1拼出27 - ak不是最优策略)
- 则拼出27仅需k - 1枚硬币,与最优策略k枚硬币矛盾
- 因此,是最优策略
2.1.2 子问题
由上述最后一步的分析,可以得到子问题的描述:求最少使用多少枚硬币能拼出27 - ak?
原问题:求最少使用多少枚硬币能拼出27?
由此可以确定状态:f(X) = 最少用多少枚硬币能拼出X
2.2 确定状态转移方程
分析:
由上述“最后一步”和“子问题”的分析,我们可以很容易得到下列公式:
f(27) = min{f(27 - 2) + 1, f(27 - 5) + 1, f(27 - 7) + 1}
- ak = 2,子问题为:最少多少枚硬币能拼出25?
- ak = 5,子问题为:最少多少枚硬币能拼出22?
- ak = 7,子问题为:最少多少枚硬币能拼出20?
由此得到状态转移方程:
f[X] = min{f[X - 2] + 1, f[X - 5] + 1, f[X - 7] + 1}
2.3 确定初始条件和边界情况
思考:如果X-2 / X- 5 / X - 7小于0怎么办?
初始条件:f[0] = 0
边界情况:f[-1] = f[-2] = … = 正无穷
边界情况设置的原因为:避免数组越界
2.4 确定计算顺序
由上述初始条件、边界情况和状态转移方程可知:该例题的计算顺序为正向(从左往右、从小到大)计算
因为:计算f[X]时,f[X - 2],f[X - 5],f[X - 7]已经得到结果了
3 例题求解
3.1 递归算法求解
def solve_coin_change(X):
"""拼出X最少需要多少枚硬币"""
if X == 0:
return 0
min_num = float('inf')
if X >= 2:
min_num = min(solve_coin_change(X - 2) + 1, min_num)
if X >= 5:
min_num = min(solve_coin_change(X - 5) + 1, min_num)
if X >= 7:
min_num = min(solve_coin_change(X - 7) + 1, min_num)
return min_num
solve_coin_change(27) # 结果为5
递归树:可以发现存在大量重复计算,导致时间复杂度为指数级别,性能很差
3.2 图示动态规划计算过程
3.3 动态规划代码
def solve_coin_change(X):
"""拼出X最少需要多少枚硬币"""
# 设置初始条件和边界情况
dp = [0] # dp[0] = 0
if X < 0:
return float('inf')
# 计算动态规划数组dp
for i in range(1, X + 1):
if i < 2:
dp.append(float('inf'))
elif i < 5:
dp.append(min(dp[i - 2] + 1, float('inf')))
elif i < 7:
dp.append(min(dp[i - 2] + 1, dp[i - 5] + 1, float('inf')))
else:
dp.append(min(dp[i - 2] + 1, dp[i - 5] + 1, dp[i - 7] + 1))
# 返回结果
return dp[X]
solve_coin_change(27) # 结果为5
3.4 动态规划时间复杂度分析
设有m种硬币,需要拼出n:
需要循环计算n次动态规划数组,每一次计算将每种硬币的选取均考虑了一次,因此时间复杂度为O(m x n)