1. 程式人生 > 其它 >14.經典動態規劃:完全揹包問題

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

。因為如果不使用任何硬幣面值,就無法湊出任何金額;如果湊出的目標金額為 0,那麼“無為而治”就是唯一的一種湊法。

我們最終想得到的答案就是dp[N][amount],其中Ncoins陣列的大小。

虛擬碼:

	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];  
}