本期任务:介绍算法中关于动态规划思想的几个经典问题
一、问题描述
"""
问题介绍
给定指定的硬币种类,面值为 1, 3, 5(在此具体化些),给定所找零的钱数 sum,给出最少的硬币找零数,每个种类的硬币无限使用。
问题分析
看到这问题,当时我想到用贪心算法来求解,最后求解方案因为巧合对了,后来在网上看到动态规划的题目,才知道贪心算法得不到最优解,
比如 给定 面值为 1, 3, 4,给定找零数为 6,用贪心法得出方案 [4,1,1],但显然 [3,3] 方案即可。
分析下问题,想一下若存在最少硬币数 num 满足当前给定的找零数 sum,则是不是一定存在最少硬币数 num-1 满足找零数 sum - (1, 3, 5 )?
答案是存在,想一想就知道,当然边界除外。这个类似于最短路径的 dijkstra算法。
输入:
w = [1, 3, 4]
s = 6
输出:
2
"""
二、算法思路
本题的解法与剪绳子问题大同小异,可以阅读【算法】【动态规划篇】第5节:剪绳子问题,加深对此类问题的理解。
1. 策略选择
一个模型:
- 硬币找零问题是典型的“多阶段决策求最优解”问题,每一次决策有最多有m种选择(m为币值种类数量),直到目标值为0或者找零失败时结束;最优解是最少的找零次数。
三个特征:
-
重复子问题:
- 不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。
- 本题中,找3零个1元或者1个三月,都可以到达目标值为3元的状态。
-
无后效性:
-
最优子结构:
- 后面阶段的状态可以通过前面阶段的状态推导出。
- 本题中,目标值为6的状态可以由目标值为5元、3元、2元的状态中的最小值加1来计算。
综上所述,本问题满足一个模型、三个特征,所以可以使用动态规划来求解。
当然,凡是能用动态规划解决的问题,都可以用回溯思想来暴力求解,具体实现代码如有兴趣可自行编写,过程不难,更多关于回溯思想的应用,可以参照:【算法】【回溯篇】第7节:0-1背包问题
2. 动态规划算法思路
动态规划使用的流程:自顶向下分析问题,自底向上解决问题!
- 使用长度为s的一维数组来记录不同目标值下的最少找零数。
- 更新过程(状态转移思路):
- 若存在最少硬币数 num 满足当前给定的找零数 sum,则一定存在最少硬币数 num-1 满足找零数 sum - (1, 3, 4)
- 若存在最少硬币数 num 满足当前给定的找零数 sum,则一定存在最少硬币数 num-1 满足找零数 sum - (1, 3, 4)
三、Python代码实现
1. 动态规划解法
class Solution():
def coins(self, w, s):
"""
使用动态规划求解硬币找零问题,若存在最少硬币数 num 满足当前给定的找零数 sum,则一定存在最少硬币数 num-1 满足找零数 sum - (1, 3, 4)
"""
res = [s + 1] * (s) # 用来记录不同问题规模下的最优解
for v in w:
if v <= s:
res[v - 1] = 1 # 找零数恰好为w中的元素,则num为1
for i in range(s): # 依次遍历res,进行填表
if res[i] == s + 1: # 只遍历未填写的位置
min_v = [res[i - v] for v in w if i - v >= 0]
if min_v: # 跳过无法找零的情形
res[i] = 1 + min(min_v)
if res[-1] > s: # 找零失败
return
else:
return res[-1]
def main():
w = [1, 3, 4]
s = 6
client = Solution()
print(client.coins(w, s))
if __name__ == '__main__':
main()
运行结果:
2