DP動態規劃 深入淺出 以Coin change II 找零方法數 為例

小松鼠-avatar-img
發佈於演算法題目解析 個房間
更新 發佈閱讀 10 分鐘


raw-image

如同這個Dynamic programming 深入淺出系列的開始,在經過比較簡單的入門題(Coin Change)之後,來看進階一點的DP題目Coin Change II


不免俗,再次強調DP的解題框架,鞏固知識點。

Dynamic programming本質是透過遞迴關係式,去解決一個大規模的問題,而這個大規模的問題又可以被分解為較小規模的子問題,而且子問題往往彼此重複


Dynamic programming的解題框架可分為三大步驟
1. 定義狀態 [我在哪裡]
2. 定義狀態轉移關係式(通則) [我從哪裡來] => [答案從哪裡推導而來]
3. 釐清初始狀態(也可以說是遞迴的終止條件) [第一步怎麼走,怎麼出發的]

題目敘述  Coin Change II

給定銅板陣列coins,和目標金額amount。
要求我們計算找零組合方法總數


測試範例

Example 1:

5 元總共有4種找零方法,分別是

5元銅板一枚

2元銅板兩枚+1元銅板一枚

2元銅板兩枚+1元銅板三枚

1元銅板五枚

Input: amount = 5, coins = [1,2,5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

Example 2:

Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

Example 3:

Input: amount = 10, coins = [10]
Output: 1

我們還是先走過一遍解題框架可分為三大步驟,藉此鞏固解題基礎。

1. 定義狀態 [我在哪裡]

這題如果寫過Coin change的人,可能會有一個直接的想法:

Easy, 這題就是前一提的變形,用類似的定義,改成相加就好。

乍看沒問題,但是我們是著走過一個小例子,看看會遇到什麼情況

定義DP[n] = n元零錢找零組合數目。

假設只有兩種銅板$1和$2,問: 找開3元有幾種組合數

DP[0] = 1 顯然0元只有一種找零組合數: 所有銅板都不拿

DP[1] = 1 顯然1元只有一種找零組合數: $1銅板一枚

DP[2] = DP[2-1] + DP[2–2] = DP[1] + DP[0] = 1 + 1 = 2

2元有兩種找零組合數: $1銅板兩枚 或 $2銅板一枚

恩…so far so good 看起來沒問題,但真的是如此嗎?


繼續看下去,看n=3的時候會發生什麼事

DP[3] = DP[3–1] + DP[3–2] = DP[2] + DP[1] = 2 + 1 = 3

這個錯的DP演算法告訴我們有3元有三種找零組合數

但是,其實3元只有兩種找零組合數:


第一種方法 $1銅板3枚 或
第二種方法 $1銅板1枚和 $2銅板1枚


那為什麼上面的DP算法會出現三種呢?

因為上面那種定義無形中已經考慮排列,

但是題目只要求組合數目,所以出現了重複計算的情狀。


DP[3]

= DP[3 - 1] (註: 這個-1代表用$1銅板一枚來湊最後一步) +

DP[3 - 2] (註:這個-2代表用$2銅板一枚來湊最後一步)

= DP[2]+ DP[1]

打開來看,看裡面發生了什麼事

DP[2] = $1銅板兩枚 或 $2銅板一枚 (+ $1銅板一枚來湊最後一步)

=> “$1銅板三枚” 或 “$2銅板一枚 + $1銅板1枚

DP[1] = $1銅板一枚 (+用$2銅板一枚來湊最後一步)

=> “$1銅板一枚 + $2銅板一枚


仔細看標粗體的地方,“$2銅板一枚 + $1銅板1枚” 和 “$1銅板一枚 + $2銅板一枚”其實是同一種組合,在DP[2]這條路徑被計入一次,在DP[1]這條路徑又被記入一次,重複了!

所以原本這個定義是錯的,不能使用


那該怎麼辦呢?


其實,DP也不限定只能一維,我們可以引入一個新的維度,代表銅板的index,一但考慮過,就前往下一個銅板的index,避免重複計算

DP[ coinIdx, n]: 代表考慮當下這個銅板coins[coinIdx],湊出n元的組合數


2. 定義狀態轉移關係式(通則)
[我從哪裡來] => [答案從哪裡推導而來]

這裡題目也沒給,不過沒關係,我們可以試者推倒看看。

用剛剛的小範例幫助我們思考

Input: amount = 3 coins =[1,2]
Output: 2

3元的找零組合數

使用兩元銅板的3元的找零組合數 + 不使用兩元銅板的3元的找零組合數

再往下展開

使用兩元銅板使用一元銅板的3元的找零組合數 +

使用兩元銅板不使用一元銅板的3元的找零組合數 +

不使用兩元銅板使用一元銅板的3元的找零組合數 +

不使用兩元銅板不使用一元銅板的3元的找零組合數

$2銅板一枚 $1銅板1枚 +

無法找開 +

$1銅板3枚 +

無法找開


所以,總共就是兩種:

$2銅板一枚 $1銅板1枚, 或 $1銅板3枚 去找開3塊錢

輸出為2

推廣到通則就是

DP(coinIdx, n )

= DP( coinIdx-1, n ) + DP( coinIdx, n — coins[coinIdx] )

不使用coins[coinIdx]元銅板的n元的找零組合數 + 使用coins[coinIdx]元銅板的n元的找零組合數


3. 釐清初始狀態(終止條件)
[第一步怎麼走,怎麼出發的]

終止條件滿明顯的,

就是n=0 (0塊錢),和n<0負數(因為負的幣值無法找零)的狀態。

0塊錢找零只有一種方法,就是不拿任何一枚銅板,return 1。


負的幣值無法找零,找零組合數為0,return 0


另外,負的coinIdx,代表沒有提供零錢去找零,

很顯然,沒有提供零錢也無法找零,找零組合數也為0,return 0


到這裡,已經填滿解題框架的所有內容,接著我們把它轉成程式碼,

這裡以Python作為示範


程式碼 DP動態規劃

class Solution:
 def change(self, amount: int, coins: List[int]) -> int:

  table = {}

  def dp( coinIdx, n ):

   if n == 0:
    # base case
    # Change for $0, only one way to do so by taking nothing
    return 1

   if (coinIdx < 0) or (n < 0):
    # base cases
    # Cannot make change without valid coins
    # Cannot make change to negative values
    return 0

   if (coinIdx, n) in table:
    # look-up table
    return table[ (coinIdx, n) ]

   # general cases
   table[ (coinIdx, n) ] = dp( coinIdx-1, n ) + dp( coinIdx, n - coins[coinIdx] )
   return table[ (coinIdx, n) ]

  # ---------------------------------

  return dp( len(coins)-1, amount)

複雜度分析

時間複雜度:O( c * n )

零錢本身一個維度,amount被找開的金額本身又是另一個維度。


空間複雜度:O( c * n )

零錢本身一個維度,amount被找開的金額本身又是另一個維度。

DP table 所耗費空間剛好就是一張二維的表格,O( c * n )


等價的迭代寫法(Iterative implementation)

輔助思考的圖解範例

raw-image
class Solution:
 def change(self, amount: int, coins: List[int]) -> int:

  # base case:
  # amount 0's method count = 1 (by taking no coins)
  change_method_count = [1] + [ 0 for _ in range(amount)]
  
  # make change with current coin, from small coin to large coin
  for cur_coin in coins:
   
   # update change method count from small amount to large amount
   for small_amount in range(cur_coin, amount+1):
    
    # current small amount can make changed with current coin
    change_method_count[small_amount] += change_method_count[small_amount - cur_coin]
    
  return change_method_count[amount]

複雜度分析

時間複雜度:O( c * n )

零錢本身一個維度,amount被找開的金額本身又是另一個維度。


空間複雜度:O( n )

amount被找開的金額本身是一個維度。

change_method_count 所耗費空間剛好就是一張一維的表格,O( n )


關鍵知識點 找零錢DP框架

強烈建議跟著複習相關的Coin Change I演算法框架統整:

合縱連橫: 找零錢的DP框架_理解背後的本質

觸類旁通: 用 DFS回溯法框架 解 組合數之和 Combination sum 全系列題。

去鞏固知識點,強化理解與認識、加深印象。


Reference

[1] Python/JS/Go/C++ O(cn) DP // Unbounded Knapsack [w/ Visualization]


回到DP特訓班目錄 和 學習路徑

留言
avatar-img
留言分享你的想法!
avatar-img
小松鼠的演算法樂園
96會員
427內容數
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
2024/09/13
給定一個整數陣列arr,和一串區間XOR請求queries。 請計算queries所請求的區間XOR值,並且以陣列的形式返回答案。
Thumbnail
2024/09/13
給定一個整數陣列arr,和一串區間XOR請求queries。 請計算queries所請求的區間XOR值,並且以陣列的形式返回答案。
Thumbnail
2024/08/27
Path with Maximum Probability 題目給定一個無向圖(雙向移動皆可), 提供每條邊的起終點,和每條邊對應的通過時的成功機率。 請問從起點start走到終點end的最高成功機率是多少? 如果完全沒有路徑可以抵達,則返回0。
Thumbnail
2024/08/27
Path with Maximum Probability 題目給定一個無向圖(雙向移動皆可), 提供每條邊的起終點,和每條邊對應的通過時的成功機率。 請問從起點start走到終點end的最高成功機率是多少? 如果完全沒有路徑可以抵達,則返回0。
Thumbnail
2024/08/21
題目敘述 664. Strange Printer 有一台奇怪的印表機, 每次操作只能連續印同樣的字母,但是列印的長度可以自由控制。 而且,印刷的時候,可以蓋過去舊的字元。 (這邊當然不合常理,讀者可以理解成塗了立可帶再蓋過去的情境) 給定一個輸入字串s,請問最少需要幾次操作,才能印出字串s?
Thumbnail
2024/08/21
題目敘述 664. Strange Printer 有一台奇怪的印表機, 每次操作只能連續印同樣的字母,但是列印的長度可以自由控制。 而且,印刷的時候,可以蓋過去舊的字元。 (這邊當然不合常理,讀者可以理解成塗了立可帶再蓋過去的情境) 給定一個輸入字串s,請問最少需要幾次操作,才能印出字串s?
Thumbnail
看更多
你可能也想看
Thumbnail
最近會試著寫一些統整類的文章, 幫助讀者、觀眾整理、吸收、複習已經學習到的演算法框架。 找零錢框架 在以前學過的題目中,我們已經學會了考零錢的抽象思考邏輯與框架,就是試著用每一種銅板去湊出n元(也就是找零錢的過程) 寫成虛擬碼或演算法,找零錢用了幾枚銅板可以這樣表達 # 銅板數目累加​
Thumbnail
最近會試著寫一些統整類的文章, 幫助讀者、觀眾整理、吸收、複習已經學習到的演算法框架。 找零錢框架 在以前學過的題目中,我們已經學會了考零錢的抽象思考邏輯與框架,就是試著用每一種銅板去湊出n元(也就是找零錢的過程) 寫成虛擬碼或演算法,找零錢用了幾枚銅板可以這樣表達 # 銅板數目累加​
Thumbnail
這題算是路徑計數類的DP衍伸題(路徑數、方法數、組合數...等等這種枚舉類的題目,第一時間切入除了想到DFS+回溯法之外,也可以留意DP動態規劃解題的可能性) 題目會給我們一個指定長度為arrLen的陣列,起點從index=0開始出發,每次移動可以往左移一格,往右移一格,或是留在原地不
Thumbnail
這題算是路徑計數類的DP衍伸題(路徑數、方法數、組合數...等等這種枚舉類的題目,第一時間切入除了想到DFS+回溯法之外,也可以留意DP動態規劃解題的可能性) 題目會給我們一個指定長度為arrLen的陣列,起點從index=0開始出發,每次移動可以往左移一格,往右移一格,或是留在原地不
Thumbnail
在學習過比較基本的DP模型 費式數列、爬樓梯、找零錢...等之後, 來看一個比較進階而且實用的DP模型,前綴和(Prefix sum), 可以再加以延伸推廣,來計算 區間和(Range Sum)。
Thumbnail
在學習過比較基本的DP模型 費式數列、爬樓梯、找零錢...等之後, 來看一個比較進階而且實用的DP模型,前綴和(Prefix sum), 可以再加以延伸推廣,來計算 區間和(Range Sum)。
Thumbnail
如同這個Dynamic programming 深入淺出系列的開始, 在經過比較簡單的入門題(Coin Change)之後, 來看比較進階的二維DP題目Unique Path
Thumbnail
如同這個Dynamic programming 深入淺出系列的開始, 在經過比較簡單的入門題(Coin Change)之後, 來看比較進階的二維DP題目Unique Path
Thumbnail
這題乍看之下是新題目,但仔細一想, 其實只是前面介紹過的費式數列的小變形而已,解題思想基本不變。 題目說每次可以爬一階樓梯或兩階樓梯,問爬到n階的方法數是多少。
Thumbnail
這題乍看之下是新題目,但仔細一想, 其實只是前面介紹過的費式數列的小變形而已,解題思想基本不變。 題目說每次可以爬一階樓梯或兩階樓梯,問爬到n階的方法數是多少。
Thumbnail
題目會給我們一個輸入陣列candidates,和一個目標值 target 問我們,從canditdates裡面重複挑選,可以湊出總和為target目標值的組合數有幾種? 在此,我們將使用找零錢II的DP模型和化簡的技巧來解題。
Thumbnail
題目會給我們一個輸入陣列candidates,和一個目標值 target 問我們,從canditdates裡面重複挑選,可以湊出總和為target目標值的組合數有幾種? 在此,我們將使用找零錢II的DP模型和化簡的技巧來解題。
Thumbnail
在經過比較簡單的入門題(Coin Change)之後, 來看進階一點的DP題目Coin Change II 整零錢的全部方法數。 不免俗,再次強調DP的解題框架,鞏固知識點。
Thumbnail
在經過比較簡單的入門題(Coin Change)之後, 來看進階一點的DP題目Coin Change II 整零錢的全部方法數。 不免俗,再次強調DP的解題框架,鞏固知識點。
Thumbnail
深入淺出,從最基本的 費式數列, 一探動態規劃的奧秘與精隨。
Thumbnail
深入淺出,從最基本的 費式數列, 一探動態規劃的奧秘與精隨。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News