動態規劃系列之九找零錢

金色旭光發表於2021-03-16

問題

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函式來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
你可以認為每種硬幣的數量是無限的。

coins = [1,2,5]
amount = 11
結果:3,硬幣為:5,5,1

解決過程

解題思路

動態規劃解題思路是:將大的問題拆解成小一點問題,小問題和大問題的解決思路是類似的
給定一個總金額11,有三種硬幣:1,2,5。
將問題的規模減少:湊11難湊,就湊10,如果10難湊就湊9,一直到湊1,湊0。

建立數學模型

新增陣列dp,表示湊到某一個數值的最小硬幣數。如dp[1]就代表金額為1的最少硬幣數,dp[10]就代表金額為10的最少硬幣數。該dp陣列長度為12,從金額為0到11,初始化為:

[12,12,12,12,12,12,12,12,12,12,12,12]

之所以初始化為12,是總金額+1,因為可能會存在湊不到這個數的情況。當湊不到時,dp[-1]=12,湊得到時,即使硬幣金額最小為1,也只用11即可。

狀態轉移方程

當要湊成的金額為0時:

dp = [0,12,12,12,12,12,12,12,12,12,12,12]

金額為1時
由於硬幣有 1、2、5,所以,金額大於硬幣1的數額,所以一塊硬幣價值為1即可

dp = [0,1,12,12,12,12,12,12,12,12,12,12]

金額為2時
金額為2是,金額大於硬幣1,硬幣2,所以有兩種方案可以湊齊。
1、某一個金額加上硬幣2,那麼就是金額0 + 硬幣2 dp[0] = 0,所以dp[2] = 1
2、某一個金額加上硬幣1,那麼就是金額1 + 硬幣1 dp[1] = 1,所以dp[2] = dp[1] + 1 = 2

選擇最小的,所以dp[2] = 1

dp = [0,1,1,12,12,12,12,12,12,12,12,12]

金額為3時
金額大於硬幣1,硬幣2,所以有兩種方案
某一個金額加上硬幣2,就是 金額1 + 硬幣2 dp[3-2] + 1。dp[3-2],意思就是金額3減去硬幣2,得到的金額1其最小的組成硬幣數。dp[3] = dp[3-1] + 1 = 2

某一個金額加上硬幣1,就是 金額2 + 硬幣1 dp[3-1] + 2。dp[3-1],意思就是 金額3 - 硬幣1,得到的金額其最小組成的硬幣數。dp[3] = dp[3-2] + 1 = 2
所以,金額3時,dp[3] = 2

dp = [0,1,1,2,12,12,12,12,12,12,12,12]

金額為4時
金額大於硬幣1,硬幣2,所以有兩種方案
金額為2 + 硬幣2,即 dp[4-2] + 1,dp[4] = 2
金額為3 + 硬幣1,即 dp[4-1] + 1,dp[4] = 3
所以,金額為4時,dp[4] = 2

dp = [0,1,1,2,2,12,12,12,12,12,12,12]

金額為5時
金額大於硬幣1,硬幣2,硬幣5,所以有三種方案
金額為4 + 硬幣1,即 dp[5-1] + 1,dp[5] = 2 + 1 = 3
金額為3 + 硬幣2,即 dp[5-2] + 1,dp[5] = 2 + 1 = 3
金額為0 + 硬幣5,即 dp[5-5] + 1,dp[5] = 1
所以,dp[5] = 1

dp = [0,1,1,2,2,5,12,12,12,12,12,12]

最終按照這個規律,算出dp所有的數值。

程式碼

示例 1:
輸入:coins = [1, 2, 5], amount = 11
輸出:3 
解釋:11 = 5 + 5 + 1

def coinChange(coins, amount):
    # 構建dp動態陣列
    dp = [amount + 1] * (amount + 1)
    # 初始化
    dp[0] = 0
    
    for i in range(1, amount + 1):
        # 每一個金額,所有能湊成的方案的硬幣數,最後取最小值
        temp = [dp[i]]
        for coin in coins:
            # 當金額大於某一個硬幣時才考慮,否則一定無法用大額硬幣湊成小額
            if i >= coin:
                temp.append(dp[i-coin]+1)
        dp[i] = min(temp)
    print(dp)
    return -1 if dp[-1] == amount + 1 else dp[-1]

coins = [1, 2, 5]
amount = 11
res = coinChange(coins, amount)
print(res)

小結

動態規劃系列到這一篇就算完結了,個人感覺動態規劃是演算法中很有難度,有技巧,有魅力的一種演算法。為了學明白這一種演算法,我也很多次在深夜裡思考求解。當我覺得自己似乎掌握了淺顯的規律之後,就把它記錄下來。這當然是我個人不太成熟的思想,動態規劃的深奧遠不是我能用9篇文章說明白的。但是,希望讀者能從這9篇文章學到一點我的經驗,摸到解決動態規劃問題的門檻。最後用我最喜歡的宮崎駿的動漫大集合作為本篇的封面。

相關文章