refer:http://interactivepython.org/courselib/static/pythonds/index.html
1. 問題描述
Tom在自動售貨機上買了一瓶飲料,售價37美分,他投入了1美元(1美元 = 100美分),現在自動售貨機需要找錢給他。售貨機中現在只有四種面額的硬幣:1美分、5美分、10美分、25美分,每種硬幣的數量充足。現在要求使用最少數量的硬幣,給Tom找錢,求出這個最少數量是多少。
2. 問題分析
自動售賣機需要給Tom找零錢63美分,而售賣機中只有四種面額的硬幣可以使用,現在的核心問題就是如何用四種面額的硬幣來湊夠63美分,並且使用的硬幣數量最少。
現在我們換個角度來思考這個問題:
是不是可以將問題規模先縮小?比如我不知道湊夠63美分最少需要多少個硬幣,那湊夠1美分、2美分的方案則顯而易見是可以馬上知道的。
為了后面敘述方便,用f(i) = n這個等式來表示這樣一種含義:湊夠i美分(0 <= i <= 63)所需要的最少硬幣數量為n個,那么我們從湊夠0美分開始寫:
-
湊0美分:因為0美分根本不需要硬幣,因此結果是0:f(0) = 0;
-
湊1美分:因為有1美分面值的硬幣可以使用,所以可以先用一個1美分硬幣,然后再湊夠0美分即可,而f(0)的值是我們已經算出來了的,所以:f(1) = 1 + f(0) = 1 + 0 = 1,這里f(1) = 1 + f(0) 中的1表示用一枚1美分的硬幣;
-
湊2美分:此時四種面額的硬幣中只有1美分比2美分小,所以只能先用一個1美分硬幣,然后再湊夠1美分即可,而f(1)的值我們也已經算出來了,所以: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美分:這時就出現了不止一種選擇了,因為有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
那么現在就可以寫出狀態轉移方程了:
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
# 找零錢問題算法實現:基本版
# 4種硬幣面值
values = [1,5,10,25]
# 湊夠amount這么多錢數需要的最少硬幣個數
def minCoins(amount):
# 需要的最少硬幣個數
ret_min = amount
if amount < 1:
ret_min = 0
# 如果要找的錢數恰好是某種硬幣的面值,那么最少只需一個硬幣
elif amount in values:
ret_min = 1
else:
# 遍歷面值數組中面值小於等於amount的那些元素
for v in [x for x in values if x <= amount]:
# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數
min_num = 1 + minCoins(amount - v)
# 判斷min_num和ret_min的大小,更新ret_min
if min_num < ret_min:
ret_min = min_num
return ret_min
def main():
print minCoins(63)
main()
將上面腳本保存成coins.py文件,在ipython中執行:%time %run coins.py,得到的結果如下:
6
CPU times: user 1min 45s, sys: 0 ns, total: 1min 45s
Wall time: 1min 45s
分析:可以看出,在我的電腦上,僅僅是為了計算用4種面額找63美分零錢,就耗時1分鍾45秒(105秒),這是無法忍受的。那么究竟為什么耗時這么巨大?下面對代碼稍加改造進行一下性能分析。
2. 性能分析
# coding:utf-8
# 找零錢問題算法實現:基本版性能分析
# 統計遞歸次數
recursion_num = 0
# 4種硬幣面值
values = [1,5,10,25]
# 湊夠amount這么多錢數需要的最少硬幣個數
def minCoins(amount):
global recursion_num
# 需要的最少硬幣個數
ret_min = amount
if amount < 1:
ret_min = 0
# 如果要找的錢數恰好是某種硬幣的面值,那么最少只需一個硬幣
elif amount in values:
ret_min = 1
else:
# 遍歷面值數組中面值小於等於amount的那些元素
for v in [x for x in values if x <= amount]:
# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數
min_num = 1 + minCoins(amount - v)
# 判斷min_num和ret_min的大小,更新ret_min
if min_num < ret_min:
ret_min = min_num
recursion_num += 1
return ret_min
def main():
print minCoins(63)
print recursion_num
main()
將上面腳本保存成coins.py文件,在ipython中執行:%time %run coins.py,得到的結果如下:
6
67716925
CPU times: user 2min, sys: 36 ms, total: 2min
Wall time: 2min
分析:可見,minCoins函數一共被遞歸調用了67716925次,真是難以想象,為了計算最多64個函數值(amount取0~63),居然遞歸調用了函數minCoins 67716925次,平均求每個值調用了1058076次。那么問題出在哪里了呢?出在了重復計算上,有很多值被重復計算了上百萬次。那么如何盡量減少重復計算呢?下面用一個緩存數組來緩存每次求出的函數值,供后面使用,從而減少重復計算。
3. 性能優化版
# coding:utf-8
# 找零錢問題算法實現:基本版性能分析
# 統計遞歸次數
recursion_num = 0
# 4種硬幣面值
values = [1,5,10,25]
# 緩存數組,為一個一維數組,用於緩存每次遞歸函數求得的值
# cache[i]表示湊夠i美分所需的最少硬幣個數,cache的元素都被初始化為-1,表示個數未知
cache = []
# 初始化緩存數組
def init(amount):
global cache
cache = [-1] * (amount + 1)
# 湊夠amount這么多錢數需要的最少硬幣個數
def minCoins(amount):
global recursion_num
global cache
# 需要的最少硬幣個數
ret_min = amount
# 如果緩存數組中有對應的值,那么直接從中取,不再重復計算了
if cache[amount] != -1:
ret_min = cache[amount]
elif amount < 1:
ret_min = 0
# 如果要找的錢數恰好是某種硬幣的面值,那么最少只需一個硬幣
elif amount in values:
ret_min = 1
else:
# 遍歷面值數組中面值小於等於amount的那些元素
for v in [x for x in values if x <= amount]:
# 用面值為v的硬幣+其他硬幣找零所需的最少硬幣數
min_num = 1 + minCoins(amount - v)
# 判斷min_num和ret_min的大小,更新ret_min
if min_num < ret_min:
ret_min = min_num
# 更新緩存數組
cache[amount] = ret_min
recursion_num += 1
return ret_min
def main():
init(63)
print minCoins(63)
print cache
print recursion_num
main()
將上面腳本保存成coins.py文件,在ipython中執行:%time %run coins.py,得到的結果如下:
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數組除了cache[0]沒被用到以外,其他元素都被利用到了,利用率還是很高的。使用緩存數組后,minCoins函數的遞歸調用次數從67716925次降低到了206次,降低了328722倍;程序耗時從105秒降低到了2.2ms,降低了47727倍,優化效果是巨大的。
上一篇動態規划之金礦模型中也使用到了緩存數組,優化效果也是巨大的,在本文中又一次看到了動態規划中緩存數組的重要性。