這是 June LeetCoding Challenge 6/7 的題目。
問題
全文:https://leetcode.com/problems/coin-change-2/
給你不同面額的硬幣與錢的總額,計算有多少種組合的硬幣可以湊出那筆錢的金額。可以假設每種硬幣都有無限多個。
手算
如果不知道怎麽解的話,可以試著代數字看看,下面用範例一的數字說明,也就是:
用 1 元、2 元及 5 元硬幣湊成 5 元,有幾種湊法?
我們可以從面額大的硬幣開始選,畫出樹狀圖:
然後把所有情形列成表格:
可以發現 5 元一共有 4 種湊法。
DFS
在上面的解法中,我們做了:
- 把硬幣種類由大排到小
- 列出第一種硬幣(5 元)的可能數量(0 個或 1 個)
- 對第一種硬幣每個可能的數量,都找出第二種硬幣(2 元)的可能數量
(0 個 5 元:0-2 個 2 元;1 個 5 元:0 個 2 元)
- 對於上面列出的硬幣組合,找出對應的第三種硬幣(1 元)數量
我們可以把這種方法寫成程式,這個程式相當於對上面的樹狀圖進行遍歷。
如果拿上面的程式進行檢測,會超過時間。不過是有用 DFS 通過的解答,像是討論中的這篇。
生成函數
先看看生成函數的定義,對一個數列 ⟨an⟩,它的生成函數是
G(x)=a0+a1x+a2x2+a3x3+⋯=k=0∑∞akxk
回到一開始的問題:
用 1 元、2 元及 5 元硬幣湊成 5 元,有幾種湊法?
Rosen 的離散數學裡有這個例題。如果數列 ⟨an⟩ 的第 k 項代表湊成 k 元的方法,我們可以寫出它的生成函數:
(1+x+x2+…)(1+x2+x4+…)(1+x5+x10+…)
乘積的第一項代表只使用 1 元硬幣的湊法,第二項代表只使用 2 元硬幣的湊法,而第三項代表只使用 5 元硬幣的湊法。
把它乘開,
1+x+2x2+2x3+3x4+4x5+…
x5 的係數是 4,所以湊成 5 元的方法數是 4 種。
我們可以用這樣的方法寫出程式:
這個程式會 TLE。不過因為這邊多項式的係數只會是 0 或 1,所以可以用這個特性簡化:
這樣就可以過了,然而這個方法相較於下面的解法,時間還是差很多。
(這則討論的解法也用到了生成函數。)
動態規劃
現在我們用不同的觀點再算一次 0-5 元的湊法。
如果先計算只有 1 元硬幣的情況:
金額 | 0 元 | 1 元 | 2 元 | 3 元 | 4 元 | 5 元 |
---|
湊法 | 1 | 1 | 1 | 1 | 1 | 1 |
因為每個金額都能、而且只能用 1 元湊出來,所以每個金額都只有 1 種湊法。
然後把 2 元硬幣考慮進去,計算有 1 元與 2 元硬幣的情況:
金額 | 0 元 | 1 元 | 2 元 | 3 元 | 4 元 | 5 元 |
---|
湊法 | 1 | 1 | 2 | 2 | 3 | 3 |
這邊,2 元除了只有 1 元硬幣的 1 種湊法以外,還有 1 個 2 元硬幣的 1 種湊法,所以是 2 種。
3 元除了只有 1 元硬幣的 1 種湊法以外,還可以拿 1 元加上 1 個 2 元硬幣。1 元有 1 種湊法,所以 3 元有 1 + 1 = 2 種。
同理,4 元除了只有 1 元硬幣的 1 種湊法以外,還可以拿 2 元加上 1 個 2 元硬幣。2 元有 2 種湊法,所以 4 元有 1 + 2 = 3 種。
再加入 5 元硬幣,可以算出:
金額 | 0 元 | 1 元 | 2 元 | 3 元 | 4 元 | 5 元 |
---|
湊法 | 1 | 1 | 2 | 2 | 3 | 4 |
因為 5 元除了前面的 3 種湊法,還可以只用 1 個 5 元硬幣湊出來,所以一共是 4 種湊法。
算到這邊,可以看出一個平常很難想出來的式子:
可以把上面的想法寫成遞迴的數學式:
前兩行是初始值,i=0 代表沒有任何硬幣。在沒有任何硬幣的時候是可以湊出 0 元的,因為只要不選硬幣就好(反正也沒有硬幣),這樣就算是一種湊法。但是沒有硬幣的時候湊不出其他金額,所以湊法有 0 種。
第三行因為 m[i,j−ci] 不存在(j−ci<0),所以不用加上這一項。
第四行就是上面文字的數學符號版本。
把上面的式子寫成程式就變成:
在上面的程式中,我們做了一個二維 vector m
,照上面的式子計算,最後得到答案 m[coins.size()][amount]
。
仔細看程式可以發現對 m[i]
的所有元素,m[i][j]
的值跟上一行的關係只在於 m[i - 1][j]
。在這一列算到 j
之前,m[i][j]
都不會被動到,這代表我們可以安心的只用一列儲存這個 vector。
把上面的程式簡化後可以得到最後的答案:
m[i][j] = m[i - 1][j];
這一行在 m
改成一維 vector 之後就沒用了,把它去掉。再把剩下的 j >= coins[i - 1]
這個條件跟 for 迴圈合併,讓 j
從 coins[i - 1]
開始就能確保上述條件永遠為真。