1. 程式人生 > >動態規劃法(二)找零錢問題

動態規劃法(二)找零錢問題

解答 time() 自己的 世界 結果 斐波那契數 自己 imp easy

??本次博客嘗試以storyline的方式來寫作,如有不足之處,還請多多包涵~~

問題的誕生

??我們故事的主人公叫做丁丁,他是一個十幾歲的小男孩,機智聰穎,是某某雜貨店的小學徒。在他生活的國度裏,只流通面額為1,3,4的硬幣。復雜這家店的店長,叫做老王,是個勤奮實幹的中年人,每天都要跟錢打交道。
??有一天,他心血來潮,叫住正在擺放貨物的丁丁,對他說道:“丁丁,你不是學過計算機方面的算法嗎?我這裏正好有個問題,不知你能解答不?”
??一聽到算法,丁丁的眼睛裏閃出光芒,這正是自己的興趣所在。於是,他連忙湊到櫃臺,好奇地問題:“什麽問題啊?”
??老王也不多說廢話,他知道丁丁的聰慧之處,直接了當地說道:“你看啊,每次顧客們買完東西付款後,我們都要找零給他們,我們這邊所有的硬幣(1,3,4)都是充足的,我想知道一共有多少種找零方式?比如說找零為4的話,就有4=1+1+1+1=3+1=1+3=4共5種方式。”
??乍聽到這個問題,丁丁有點蒙圈了,因為4的情況是簡單的,但是隨著找零的面額增加,數量的變化就沒有什麽規律了。他示意掌櫃出去走走,掌櫃也欣然同意。

遞歸?動態規劃?

??此時我們的主人公正坐在湖邊靜靜地思考,腦海中湧現出各種各樣的計算機算法。突然,遞歸法進入了他的視野,對,就是遞歸法!他認真地整理著思路:

  1. 考慮面額為n的情況,假設\(n=x_{1}+x_{2}+...+x_{m}\).那麽,只需考慮最後一個數\(x_{m}=1,3,4\)的情形。當\(x_{m}=1,3,4\),剩下的面額為\(n-1,n-3,n-4.\)
  2. 假設面額為n的找零方式為\(f(n)\),則\(f(n)=f(n-1)+f(n-3)+f(n-4)\),這樣就能按照遞歸法來做了。
  3. 最後,只需要確定初值即可,\(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), 歡迎大家關註哦~~

動態規劃法(二)找零錢問題