$动态规划系列(2)——找零钱问题
refer:http://interactivepython.org/courselib/static/pythonds/index.html
1. 問題描述
Tom在自動售貨機(jī)上買了一瓶飲料,售價37美分,他投入了1美元(1美元 = 100美分),現(xiàn)在自動售貨機(jī)需要找錢給他。售貨機(jī)中現(xiàn)在只有四種面額的硬幣:1美分、5美分、10美分、25美分,每種硬幣的數(shù)量充足。現(xiàn)在要求使用最少數(shù)量的硬幣,給Tom找錢,求出這個最少數(shù)量是多少。
2. 問題分析
自動售賣機(jī)需要給Tom找零錢63美分,而售賣機(jī)中只有四種面額的硬幣可以使用,現(xiàn)在的核心問題就是如何用四種面額的硬幣來湊夠63美分,并且使用的硬幣數(shù)量最少。
現(xiàn)在我們換個角度來思考這個問題:
是不是可以將問題規(guī)模先縮小?比如我不知道湊夠63美分最少需要多少個硬幣,那湊夠1美分、2美分的方案則顯而易見是可以馬上知道的。
為了后面敘述方便,用f(i) = n這個等式來表示這樣一種含義:湊夠i美分(0 <= i <= 63)所需要的最少硬幣數(shù)量為n個,那么我們從湊夠0美分開始寫:
湊0美分:因為0美分根本不需要硬幣,因此結(jié)果是0:f(0) = 0;
湊1美分:因為有1美分面值的硬幣可以使用,所以可以先用一個1美分硬幣,然后再湊夠0美分即可,而f(0)的值是我們已經(jīng)算出來了的,所以:f(1) = 1 + f(0) = 1 + 0 = 1,這里f(1) = 1 + f(0) 中的1表示用一枚1美分的硬幣;
湊2美分:此時四種面額的硬幣中只有1美分比2美分小,所以只能先用一個1美分硬幣,然后再湊夠1美分即可,而f(1)的值我們也已經(jīng)算出來了,所以:f(2) = 1 + f(1) = 1 + 1 = 2,這里f(2) = 1 + f(1) 中的1表示用一枚1美分的硬幣;
湊3美分:和上一步同樣的道理,f(3) = 1 + f(2) = 1 + 2 = 3;
湊4美分:和上一步同樣的道理,f(4) = 1 + f(3) = 1 + 3 = 4;
湊5美分:這時就出現(xiàn)了不止一種選擇了,因為有5美分面值的硬幣。方案一:使用一個5美分的硬幣,再湊夠0美分即可,這時:f(5) = 1 + f(0) = 1 + 0 = 1,這里f(5) = 1 + f(0) 中的1表示用一枚5美分的硬幣;方案二:使用1個1美分的硬幣,然后再湊夠4美分,此時:f(5) = 1 + f(4) = 1 + 4 = 5。綜合方案一和方案二,可得:f(5) = min{1 + f(0),1 + f(4)} = 1;
湊6美分:此時也有兩種方案可選,方案一:先用一個1美分,然后再湊夠5美分即可,即:f(6) = 1 + f(5) = 1 + 1 = 2;方案二:先用一個5美分,然后再湊夠1美分即可,即:f(6) = 1 + f(1) = 1 + 1 = 2。綜合兩種方案,有:f(6) = min{1 + f(5), 1 + f(1)} = 2;
...(省略)
從上面的分析過程可以看出,要湊夠i美分,就要考慮如下各種方案的最小值:
1 + f(i - value[j]),其中value[j]表示第j種(j從0開始,0 <= j < 4)面值且value[j] <= i
那么現(xiàn)在就可以寫出狀態(tài)轉(zhuǎn)移方程了:
f(i) = 0, i = 0
f(i) = 1, i = 1
f(i) = min{1 + f(i - value[j])}, i > 1,value[j] <= i
3. Talk is cheap, show the code
1. 基本版
# coding:utf-8 # 找零錢問題算法實(shí)現(xiàn):基本版# 4種硬幣面值 values = [1,5,10,25]# 湊夠amount這么多錢數(shù)需要的最少硬幣個數(shù) def minCoins(amount):# 需要的最少硬幣個數(shù)ret_min = amountif amount < 1:ret_min = 0# 如果要找的錢數(shù)恰好是某種硬幣的面值,那么最少只需一個硬幣elif amount in values:ret_min = 1else:# 遍歷面值數(shù)組中面值小于等于amount的那些元素for v in [x for x in values if x <= amount]:# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數(shù)min_num = 1 + minCoins(amount - v)# 判斷min_num和ret_min的大小,更新ret_minif min_num < ret_min:ret_min = min_numreturn ret_mindef main():print minCoins(63)main()將上面腳本保存成coins.py文件,在ipython中執(zhí)行:%time %run coins.py,得到的結(jié)果如下:
6
CPU times: user 1min 45s, sys: 0 ns, total: 1min 45s
Wall time: 1min 45s
分析:可以看出,在我的電腦上,僅僅是為了計算用4種面額找63美分零錢,就耗時1分鐘45秒(105秒),這是無法忍受的。那么究竟為什么耗時這么巨大?下面對代碼稍加改造進(jìn)行一下性能分析。
2. 性能分析
# coding:utf-8 # 找零錢問題算法實(shí)現(xiàn):基本版性能分析# 統(tǒng)計遞歸次數(shù) recursion_num = 0# 4種硬幣面值 values = [1,5,10,25]# 湊夠amount這么多錢數(shù)需要的最少硬幣個數(shù) def minCoins(amount):global recursion_num# 需要的最少硬幣個數(shù)ret_min = amountif amount < 1:ret_min = 0# 如果要找的錢數(shù)恰好是某種硬幣的面值,那么最少只需一個硬幣elif amount in values:ret_min = 1else:# 遍歷面值數(shù)組中面值小于等于amount的那些元素for v in [x for x in values if x <= amount]:# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數(shù)min_num = 1 + minCoins(amount - v)# 判斷min_num和ret_min的大小,更新ret_minif min_num < ret_min:ret_min = min_numrecursion_num += 1return ret_mindef main():print minCoins(63)print recursion_nummain()將上面腳本保存成coins.py文件,在ipython中執(zhí)行:%time %run coins.py,得到的結(jié)果如下:
6
67716925
CPU times: user 2min, sys: 36 ms, total: 2min
Wall time: 2min
分析:可見,minCoins函數(shù)一共被遞歸調(diào)用了67716925次,真是難以想象,為了計算最多64個函數(shù)值(amount取0~63),居然遞歸調(diào)用了函數(shù)minCoins 67716925次,平均求每個值調(diào)用了1058076次。那么問題出在哪里了呢?出在了重復(fù)計算上,有很多值被重復(fù)計算了上百萬次。那么如何盡量減少重復(fù)計算呢?下面用一個緩存數(shù)組來緩存每次求出的函數(shù)值,供后面使用,從而減少重復(fù)計算。
3. 性能優(yōu)化版
# coding:utf-8 # 找零錢問題算法實(shí)現(xiàn):基本版性能分析# 統(tǒng)計遞歸次數(shù) recursion_num = 0# 4種硬幣面值 values = [1,5,10,25]# 緩存數(shù)組,為一個一維數(shù)組,用于緩存每次遞歸函數(shù)求得的值 # cache[i]表示湊夠i美分所需的最少硬幣個數(shù),cache的元素都被初始化為-1,表示個數(shù)未知 cache = []# 初始化緩存數(shù)組 def init(amount):global cachecache = [-1] * (amount + 1)# 湊夠amount這么多錢數(shù)需要的最少硬幣個數(shù) def minCoins(amount):global recursion_numglobal cache# 需要的最少硬幣個數(shù)ret_min = amount# 如果緩存數(shù)組中有對應(yīng)的值,那么直接從中取,不再重復(fù)計算了if cache[amount] != -1:ret_min = cache[amount]elif amount < 1:ret_min = 0# 如果要找的錢數(shù)恰好是某種硬幣的面值,那么最少只需一個硬幣elif amount in values:ret_min = 1else:# 遍歷面值數(shù)組中面值小于等于amount的那些元素for v in [x for x in values if x <= amount]:# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數(shù)min_num = 1 + minCoins(amount - v)# 判斷min_num和ret_min的大小,更新ret_minif min_num < ret_min:ret_min = min_num# 更新緩存數(shù)組cache[amount] = ret_minrecursion_num += 1return ret_mindef main():init(63)print minCoins(63)print cacheprint recursion_nummain()將上面腳本保存成coins.py文件,在ipython中執(zhí)行:%time %run coins.py,得到的結(jié)果如下:
6
[-1, 1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7, 3, 4, 5, 6]
206
CPU times: user 4 ms, sys: 0 ns, total: 4 ms
Wall time: 2.2 ms
分析:可見,cache數(shù)組除了cache[0]沒被用到以外,其他元素都被利用到了,利用率還是很高的。使用緩存數(shù)組后,minCoins函數(shù)的遞歸調(diào)用次數(shù)從67716925次降低到了206次,降低了328722倍;程序耗時從105秒降低到了2.2ms,降低了47727倍,優(yōu)化效果是巨大的。
上一篇動態(tài)規(guī)劃之金礦模型中也使用到了緩存數(shù)組,優(yōu)化效果也是巨大的,在本文中又一次看到了動態(tài)規(guī)劃中緩存數(shù)組的重要性。
轉(zhuǎn)載于:https://www.cnblogs.com/jiayongji/p/7118895.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的$动态规划系列(2)——找零钱问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 惠新宸php教程_惠新宸:PHP在百度的
- 下一篇: c语言矩阵键盘反转法扫描,行列扫描法,反