許多人認為身上如果帶太多的零錢會讓行動變得不方便,因此會希望商店店員在找零錢的時候能夠以最少的硬幣數來找,而不是全部都用1元塞給我們。



這篇文章將會直接以動態規劃來解決問題,如果您還不熟悉動態規劃的話可以先查看這篇文章:

https://magiclen.org/dynamic-programming-basic/

假設現在店家有1、5、10、25元面額的硬幣,不限數量,當店員要找給客人31元的時候,可以怎麼找?例如:

  1. 給31個1元。(那店員怕是要被揍了)
  2. 給1個25元、1個5元、1個1元。
  3. 給3個10元、1個1元。

店員如果是拿1個25元、1個5元、1個1元,只需要3枚硬幣就可以湊成31元,而客人拿了也會很開心。

您可能會覺得,這問題不是很簡單嗎?只要從最大面額開始找就好了。確實沒錯,但如果多了一種21元的硬幣面額會怎麼樣呢?

重新敘述問題:假設現在店家有1、5、10、21、25元面額的硬幣,不限數量,當店員要找給客人63元的時候,最少要多少個硬幣?

如果我們從最大面額開始找,需要2個25元,1個10元,3個3元,共6枚硬幣,但這個找法卻不是這個問題的最佳解……因為我們其實可以只用3個21元的硬幣來組成63元。當然,也是因為這個題目已經設計好的關係(63明顯就是21乘3),才能讓我們快速地找出最佳解,如果換成其它數字,可能就不是那麼容易了。

以Top-down的方式來思考上面的問題。因為在最後一步要能夠以最少硬幣數湊出63元,而在執行最後一步前,有可能已經湊出了62元、58元、42元或是38元,所以要從0元湊到63元,最少的硬幣數就是湊到62元的最少硬幣數、湊到58元的最少硬幣數、湊到42元的最少硬幣數和湊到38元的最少硬幣數中的最小值再加1……依此類推。最小的問題為湊0元(最少需要0個硬幣)、湊1元(最少需要1個硬幣)和湊2 ~ 24元,但是湊2 ~ 24元我們不太能夠直接知道最少要幾個硬幣,所以不要把它當作是基本案例(base case)。

以程式來解題的話,我們可以寫出以下的遞迴函數:

當然,這個函數的效能很差,先不管它。因為還有個更嚴重的地方,那就是這個程式目前只能固定地用1、5、10、21、25元面額的硬幣來計算,功能實在太弱了!所以我們要把程式改成可以從函數參數輸入硬幣的面額(假設至少有一種面額,每種面額至少會有1元,且會遞增輸入)。而因為輸入的最小面額可能會大於1元,所以需要注意到無解的情形(例如面額只有3元和7元,就不可能湊到11元)。若是無解,則把結果設為「最大整數」。

程式強化功能後如下:

由於以上程式碼有重疊子問題,所以可以用動態規劃的記憶法來優化或是用製表法將其改寫成迭代函數。

以製表法為例,改寫如下:

dp陣列的索引i所儲存的值就是湊到i元時最少需要的硬幣數量。

接著我們就要來利用上面這支程式去取得最少硬幣數量的組合方式了。

為了讓各位充分理解上面這支程式的用途,就先把程式改為直接在螢幕上輸出結果,而不是透過函數回傳出來。程式碼如下:

想要知道硬幣的組合方式,可以再添加一個長度為amount + 1的陣列,這個陣列裡面的元素都是長度為硬幣面額數量的陣列,且數值初始化為0,用來儲存湊到i元時,各種面額的硬幣的數量。程式碼如下:

如果您覺得以上程式碼會吃掉太多記憶體空間,而且計算過程中還要在那邊複製陣列,看起來不是很好的話,其實也可以考慮只用長度為amount + 1的一維陣列,去記錄每個階段的問題在最佳解時最後拿到的硬幣面額(的索引值)。最後再反推回去就可以知道到底拿了哪些面額的硬幣了。