本次博客嘗試以storyline的方式來寫作,如有不足之處,還請多多包涵~~
問題的誕生
我們故事的主人公叫做丁丁,他是一個十幾歲的小男孩,機智聰穎,是某某雜貨店的小學徒。在他生活的國度里,只流通面額為1,3,4的硬幣。復雜這家店的店長,叫做老王,是個勤奮實干的中年人,每天都要跟錢打交道。
有一天,他心血來潮,叫住正在擺放貨物的丁丁,對他說道:“丁丁,你不是學過計算機方面的算法嗎?我這里正好有個問題,不知你能解答不?”
一聽到算法,丁丁的眼睛里閃出光芒,這正是自己的興趣所在。於是,他連忙湊到櫃台,好奇地問題:“什么問題啊?”
老王也不多說廢話,他知道丁丁的聰慧之處,直接了當地說道:“你看啊,每次顧客們買完東西付款后,我們都要找零給他們,我們這邊所有的硬幣(1,3,4)都是充足的,我想知道一共有多少種找零方式?比如說找零為4的話,就有4=1+1+1+1=3+1=1+3=4共4種方式。”
乍聽到這個問題,丁丁有點蒙圈了,因為4的情況是簡單的,但是隨着找零的面額增加,數量的變化就沒有什么規律了。他示意掌櫃出去走走,掌櫃也欣然同意。
遞歸?動態規划?
此時我們的主人公正坐在湖邊靜靜地思考,腦海中涌現出各種各樣的計算機算法。突然,遞歸法進入了他的視野,對,就是遞歸法!他認真地整理着思路:
- 考慮面額為n的情況,假設\(n=x_{1}+x_{2}+...+x_{m}\).那么,只需考慮最后一個數\(x_{m}=1,3,4\)的情形。當\(x_{m}=1,3,4\),剩下的面額為\(n-1,n-3,n-4.\)
- 假設面額為n的找零方式為\(f(n)\),則\(f(n)=f(n-1)+f(n-3)+f(n-4)\),這樣就能按照遞歸法來做了。
- 最后,只需要確定初值即可,\(f(0)=f(1)=f(2)=1,f(3)=2.\)
問題似乎到這就解決了,因為有了這個遞推式,那么,直接定義一個函數就能解決問題了。等等,他想起昨天看到的博客“動態規划法(一)從斐波那契數列談起”。對了,對於遞推式,可以用動態規划法解決啊。於是,他順手寫了一下Python代碼:
import time
# calculate the number of ways of integer n can be write the sum of 1,3,4
def sum_part_dp(n):
if n <= 2:
return 1
elif n == 3:
return 2
first = 1
second = 1
third = 1
fourth = 2
# repeat n-3 times
for _ in range(n-3):
answer = first + second + fourth
first = second
second = third
third = fourth
fourth = answer
return fourth
n = 40
t1 = time.time()
s = sum_part_dp(n)
t2 = time.time()
print('面額:%s,方法數:%s,耗時:%s'%(n, s, t2-t1))
他迅速地敲完了以上代碼,運行,得到結果:
面額:40,方法數:119814916,耗時:0.0
Bingo,搞定!他滿懷欣喜地將這個結果告訴了掌櫃老王,老王看了,也禁不住點點頭,心想:計算機算法真有用啊!
再一次的挑戰
可是老王也是一個有想法的人,他看着丁丁這么干脆利落地解決了這個問題,決心再出一個難題考考他。他清了清喉嚨,對丁丁說道:“剛才的問題解答得很棒啊,值得表揚 !但是現在呢,我這又有個麻煩事。每次找零,怎樣找零才能使得找零的硬幣數最少呢?”
丁丁笑而不語,他點了點頭,就抱着他的電腦離開了。老王望着他離去的背影,心想:這個問題要是能解決,以后找零也就省了不少麻煩。不知這次丁丁要用多長時間?
有了上個問題的積累,丁丁對於解決這個問題滿懷信心。還是跟剛才的解答方法一樣,先用遞歸,假設面額為\(n\)的找零所用最少硬幣數為\(f(n)\),則\(f(n)=min\{f(n-1)+1,f(n-3)+1,f(n-4)+1\}.\)采用自底向上的動態規划法,記錄每個子問題的解,避免重復求解,這樣就能得到\(f(n)\)的值了。那么,怎樣才能記錄每個子問題的解呢?用Python中的字典啊!這樣,硬幣數量是得到了,可是具體的找零方式呢?不難,只要用一個變量記錄剛才表達式中是取\(f(n-1)\)還是\(f(n-3)\)還是\(f(n-4)\),對應面額為1,3,4,再遞歸地求解下去即可。
他寫下了Python代碼:
# 找零錢問題
# 找零錢字典,key為面額,value為最小硬幣數
change_dict = {}
# 動態規划法解決問題
# 時間復雜度:多項式時間
# 只求解最小的硬幣數量
def rec_change(M, coins):
change_dict[0] = 0
s = 0
for money in range(1, M+1):
num_of_coins = float('inf')
for coin in coins:
if money >= coin:
# 記錄每次所用的硬幣數量
if change_dict[money-coin]+1 < num_of_coins:
num_of_coins = change_dict[money-coin]+1
s = coin #記錄每次找零的面額
change_dict[money] = num_of_coins
return change_dict[M],s
# 求出具體的找零方式
# 用path變量記錄每次找零的面額
def method(M, coins):
print('Total denomination is %d.'%M)
nums, path = rec_change(M, coins)
print('The smallest number of coins is %d.'%nums)
print('%s'%path, end='')
while M-path > 0:
M -= path
nums, path = rec_change(M, coins)
print(' -> %s'%path, end='')
print()
coins = (1, 3, 4)
method(50, coins)
運行結果如下:
Total denomination is 50.
The smallest number of coins is 13.
3 -> 3 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4 -> 4
幾分鍾后,當掌櫃老王看到這個結果后,驚訝得目瞪口呆!在這家小小的雜貨店里,也許藏着一位計算機天才,他這樣想到。
而我們的主人公呢?此時,他已經向着斜陽,走在縣城的小道上,躊躇滿志,准備着去外面的世界看一看~~
注意:本人現已開通兩個微信公眾號: 用Python做數學(微信號為:python_math)以及輕松學會Python爬蟲(微信號為:easy_web_scrape), 歡迎大家關注哦~~