14.經典動態規劃:完全揹包問題
零錢兌換②(LeetCode 518題 難度:中等)
我們可以把這個問題轉化為揹包問題的描述形式:
有一個揹包,最大容量為amount
,有一系列物品coins
,每個物品的重量為coins[i]
,每個物品的數量無限。請問有多少種方法,能夠把揹包恰好裝滿?
這個問題和我們前面講過的兩個揹包問題,有一個最大的區別就是,每個物品的數量是無限的,這也就是傳說中的「完全揹包問題」,沒啥高大上的,無非就是狀態轉移方程有一點變化而已。
解題思路
第一步要明確兩點,「狀態」和「選擇」。
狀態有兩個,就是「揹包的容量」
和「可選擇的物品」
,選擇就是「裝進揹包」
或者「不裝進揹包」
。
明白了狀態和選擇,動態規劃問題基本上就解決了,只要往這個框架套就完事兒了:
for狀態1in狀態1的所有取值:
for狀態2in狀態2的所有取值:
for...
dp[狀態1][狀態2][...]=擇優(選擇1,選擇2...)
第二步要明確****dp
陣列的定義。
首先看看剛才找到的「狀態」,有兩個,也就是說我們需要一個二維dp
陣列。
dp[i][j]
的定義如下:
若只使用前i
個物品,當揹包容量為j
時,有dp[i][j]
種方法可以裝滿揹包。
換句話說,翻譯回我們題目的意思就是:
若只使用coins
中的前i
個硬幣的面值,若想湊出金額j
,有dp[i][j]
種湊法。
經過以上的定義,可以得到:
base case 為dp[0][..] = 0, dp[..][0] = 1
我們最終想得到的答案就是dp[N][amount]
,其中N
為coins
陣列的大小。
虛擬碼:
intdp[N+1][amount+1]
dp[0][..]= 0
dp[..][0]= 1
for i in [1..N]:
for j in [1..amount]:
把物品i裝進揹包,
不把物品i裝進揹包
return dp[N][amount]
第三步,根據「選擇」,思考狀態轉移的邏輯。
注意,我們這個問題的特殊點在於物品的數量是無限的,所以這裡和之前寫的揹包問題文章有所不同。
如果你不把這第i
個物品裝入揹包,也就是說你不使用coins[i]
這個面值的硬幣,那麼湊出面額j
的方法數dp[i][j]
應該等於dp[i-1][j]
,繼承之前的結果。
如果你把這第i
個物品裝入了揹包,也就是說你使用coins[i]
這個面值的硬幣,那麼dp[i][j]
應該等於dp[i][j-coins[i-1]]
。
首先由於i
是從 1 開始的,所以coins
的索引是i-1
時表示第i
個硬幣的面值。
dp[i][j-coins[i-1]]
也不難理解,如果你決定使用這個面值的硬幣,那麼就應該關注如何湊出金額j - coins[i-1]
。
比如說,你想用面值為 2 的硬幣湊出金額 5,那麼如果你知道了湊出金額 3 的方法,再加上一枚面額為 2 的硬幣,不就可以湊出 5 了嘛。
綜上就是兩種選擇,而我們想求的dp[i][j]
是「共有多少種湊法」,所以dp[i][j]
的值應該是以上兩種選擇的結果之和:
for (int i= 1;i<=n;i++){
for (int j= 1;j<=amount;j++){
if (j-coins[i-1]>= 0)
dp[i][j]=dp[i- 1][j] +dp[i][j-coins[i-1]];
return dp[N][W]
最後一步,把偽碼翻譯成程式碼,處理一些邊界情況。
intchange(intamount,int[]coins) {
int n=coins.length;
int[][]dp=amount int[n+ 1][amount+ 1];
//basecase
for (int i= 0;i<=n;i++)
dp[i][0]= 1;
for (int i= 1;i<=n;i++){
for (int j= 1;j<=amount;j++)
if (j-coins[i-1]>= 0)
dp[i][j]=dp[i- 1][j]
+dp[i][j-coins[i-1]];
else
dp[i][j]=dp[i- 1][j];
}
return dp[n][amount];
}