動態規劃之揹包九講之二 — 完全揹包

在講解之前我們先來看一下題目。如下:
一個小偷有一個最大容納M千克的揹包,現在商店裏有N件物品,每種物品的個數是無限的,每件物品的的重量分別是w1,w2,…wn,每件物品的價值爲v1,v2,…,vn。求小偷能偷走的最大價值。(其中M<=200,n<=30)。
第一行輸入M,n。第二行到第n+1行,每行輸入一個重量和一個價值,代表第i件物品的信息。

承接01揹包問題,本題在01揹包基礎上添加了一個新的條件,即每一種物品的個數是無限的。所以我們可以在01揹包問題的解決辦法之上進行添加修改,來得到這一問題的答案。
有了前面01揹包的詳解,在這裏我們不再從頭開始問題分析。(如需要回顧01揹包問題的問題分析,附01揹包鏈接:動態規劃之揹包九講之一 — 01揹包)首先我們分析這兩個問題的本質區別是什麼。對於01揹包來說,每一種都是唯一的,只能拿一次。但是對於完全揹包每一種物品可以拿多次,所以我們只需要在01揹包基礎上增加一層循環即可。
初級代碼如下:(壓縮後的代碼,時間複雜度:O(n3))

#include<iostream>
#include<algorithm>
using namespace std;
int dp[210], w[40], v[40];
int main()
{
	int n, M;
	cin >> M >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> w[i] >> v[i];
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = M; j >= w[i]; j--)
		{
			for (int k = 0; k <= j / w[i]; k++)
			{
				dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i]);
			}
		}
	}
	cout << dp[M];
	return 0;
}

我們嘗試能否對上面給出的代碼進行優化,首先要推導本題的狀態轉移方程。令f[ i ][ j ]表示在前i件物品中,當前揹包容量爲 j 時,所能獲得的最大價值。由於第 i 種物品可以拿多件,只要揹包裝得下就可以,所以在考慮第i種物品時,可以選擇拿0件,1件,…,k件(k代表第 i 種物品能拿最多的件數,也就是說當拿k+1件時,揹包裝不下了。)所以推導狀態轉移方程如下:

1.dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i], dp[i-1][j-2*w[i]]+2*v[i],...,dp[i-1][j-k*w[i]]+k*v[i])
2.dp[i][j-w[i]]=max(dp[i-1][j-w[i]], dp[i-1][j-2*w[i]]+v[i],..., dp[i-1][j-k*w[i]]+(k-1)*v[i])

通過觀察規律可以得到最終表達式(狀態轉移方程):

dp[i][j]=max(dp[i-1][j], dp[i][j-w[i]]+v[i])

我們假設輸入的數據如下 : (結果:12)
10 4
2 1
3 3
4 5
7 9

通過在循環中打印當前值可以得到二維數組的值,如下:
在這裏插入圖片描述
對錶格數據的計算途徑分析以及狀態轉移方程可以發現我們要求出的dp[ i ][ j ]只與它正上方的元素以及第 i 行的某個元素有關。所以類比01揹包問題我們進行優化數組(滾動數組思想)。而且 j - w[ i ] 一定小於 j ,對應到二維數組裏的位置上就是在dp[ i ][ j ]的左邊。(藍色箭頭&黃色箭頭爲一組,綠色箭頭&黑色箭頭爲一組)。這裏就是01揹包中再三強調的壓縮順序問題,本題區別於01揹包,01揹包從後往前更新數組,而這裏我們需要從前往後更新數組。(因爲我們需要用到第 i 行的最新值所以我們可以得到高級代碼如下(時間複雜度:O(n2))

#include<iostream>
#include<algorithm>
using namespace std;
int dp[210], w[40], v[40];
int main()
{
	int n, M;
	cin >> M >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> w[i] >> v[i];
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = w[i]; j <= M; j++)   //省去一層循環 k
		{
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}
	cout << dp[M];
	return 0;
}

總結
對於01揹包問題,首先確定狀態轉移方程,然後通過分析狀態轉移方程發現每一次更新dp[ i ][ j ]的數據需要用到該位置正上方的元素以及該位置上一行的左邊(左上方)的元素,所以在應用滾動數組時要從後向前更新數據,否則出現數據覆蓋。而完全揹包恰恰相反,每一次更新dp[ i ][ j ]的數據需要用到該位置正上方的元素以及該位置的左邊(正左方)的元素,所以在應用滾動數組時要從前向後更新數據。(這是在壓縮時二者最大的區別)。

到此爲止,完全揹包問題的所有解法,由淺入深都講解完畢。後續還會更新揹包問題的其他分支,如果喜歡就點個讚唄。謝謝大家。

ps:博主能力有限,如果讀者發現什麼問題,歡迎私信或評論指出不足。歡迎讀者詢問問題,樂意盡我所能解答讀者的問題。歡迎評論,歡迎交流。謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章