今天打開 leetcode,點進去它推送的 daily challenge,然後就成為了今天噩夢的開始...我要好好了解演算法了 QQ
題目是 518. Coin Challenge II,題目要求不難理解,就是規定有一個要湊出來的金額 amount,然後有一袋硬幣 coins,硬幣面額有限但數量無限,請問有多少湊法?
舉例來說,就是如果有 1、2、5 三種面額的硬幣,要湊成 5 元可以有幾種湊法,答案是 4 種:
不難理解題目的意思吧。然後真的開始寫就爆炸了,我的 code 歷經了多輪測試層層挺進,最後死在了要用 1、2、5 去湊出 500 元來,給大家看一下有多慘哈...
所以我忍不住了,直接點開 solutions 看大神怎麼解,然後歪唷,嗯,看某...還跑去問了 ChatGPT 才知道這用的是動態規劃的解法,而這題也是動態規劃的經典題。經典是經典,但為難了我這還沒出新手村的孩砸...。
Code 是這樣子的:
var change = function(amount, coins) {
const dp = new Array(amount + 1).fill(0);
dp[0] = 1;
for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
一個陣列、兩個迴圈,簡簡單單,然後我跟 ChatGPT 拉扯了幾個小時、爬了幾篇動態規劃的文章,後來在中午吃飯時看了下面兩部影片才算是稍微知道這串 code 背後的邏輯思維是啥。我就稍微記錄一下我的理解哈,如果有理解錯拜託小力鞭 QQ
所以這串 code 到底搞了什麼事情,我嘗試理解拆解了一下。
1.) 建立了一個長度為 amount + 1 的陣列 "dp",它是用來儲存湊成金額 "i" 的方法數量的陣列。因為還沒開始更新,所以先把值都設為 0。
好了,為何要一個個把金額 i 列出來?題目不是只要求要湊到 amount 就好嗎?我後來理解這就是有關動態規劃的 "把大問題拆成很多小問題,然後由小問題的解組合成大問題的解" 的概念所在,很渾沌對吧?用下面例子說應該會比較清楚一點...
2.) 先建立了一個陣列後,把 dp[0] 設為 1,這裡的意思很簡單,就是湊成金額 0 的方法只有一個,就是都不拿任何硬幣。
3.) 開始第一遍遍歷 (coin = 1),我們計算出使用硬幣面額 1 去湊出目標金額 i (1 ~ 5) 的方法有幾種。答案是都是一種,因為 "1 = 1"、"2 = 1 + 1"、"3 = 1+ 1 + 1"...以此類推,到這裡都還不難理解齁。
4.) 第二遍遍歷 (coin = 2),噩夢的開始,天知道我花了多久在思考這裡。我們先再看一次內層的迴圈。
for (let i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
可以知道我們硬幣 2 要從目標金額 2 (i = 2) 開始湊了。但為什麼是dp[i] += dp[i - coin]
?
先再重申一次 dp[i] 是儲存湊成 i 金額方法數的地方,所以 dp[2] 就是儲存湊成 2 的方法數的地方,再一次強調,免得待會打結。
好,現在來看一下下面這張圖,我用紅色和藍色分別標記了兩個 1,是什麼意思?是這樣的:
我們接著看 dp[3]:
再一次啊,來看 dp[4]:
所以到這裡可以知道dp[i] += dp[i - coin]
這裡面dp[i]
代入的是前一次遍歷更新後湊成 i 的方法數,dp[i - coin]
代表著使用了一次這個硬幣後,我們還需要湊成剩餘金額 "i - coin" 的湊法數量,而這個數量我們已經在前面的迴圈中儲存在 dp 中了。
到這裡我們可以看到,每次代入的參數其實都是前一次遍歷和迴圈帶來的結果,這樣一層一層堆砌成最後的解答,動態規劃的概念這就出現了。
最後我們會得到下面這張圖,也就是此時 dp = [1, 1, 2, 2, 3, 4]。而 4 就是我們最終的解答。
e04!真的搞了我好久,我要好好去學演算法了...
參考資料: