最簡單的背包問題——0/1背包問題


背包問題是一種組合優化的 NP 完全問題:有 N 個物品和容量為 W 的背包,每個物品都有自己的體積 w 和價值 v,求拿哪些物品可以使得背包所裝下物品的總價值最大。如果限定每種物品只能選擇 0 個或 1 個,則問題稱為 0-1 背包問題;如果不限定每種物品的數量,則問題稱為無界背包問題或完全背包問題。

下面我們就先來介紹一下背包問題中最簡單的0/1背包問題

還是上面的情景:我們有N個物品和容量為W的背包,每個物品的體積為wi,價值為vi,每個物品最多拿一次。求背包容量不超過W的情況下所能拿的最大價值。

使用動態規划解題第一步:定義狀態量

題目中有兩個變量與最終結果相聯系——物品數量與容積,因此我們用一個二維數組dp[i][j]來表示考慮前i個物品,在背包容量不超過j時背包中物品的最大價值。

第二步:狀態轉移

在我們遍歷到第i件物品時,當前背包容量為j的情況下,我們有兩種選擇——不將第i件物品放入包內,那么背包內的總價值狀態就等於第i-1件物品,背包容量為j時的狀態,即dp[i][j]=dp[i-1][j]。如果我們要將第i件物品放入包內,那么此時背包中的總狀態量應當等於遍歷到第i-1件物品,且背包容量等於j-w[i]時的狀態再加上第i件物品的價值。即,dp[i][j]=dp[i-1][j-wi]+vi

代碼如下:

int knapsack(vector<int> Values,vector<int> Weight,int W,int N){
    vector<vector<int>> dp(N+1,vector<int>(W+1,0));
    for(int i=1;i<=N;i++){
        for(int j=1;j<=W;j++){
            if(j>=Weight[i-1]){
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-Weight[i-1]]+Values[i-1]);
            }
            else{
                dp[i][j]=dp[i-1][j];
            }
        }
    }
    return dp[N][W];
}

優化:我們可以看到上述代碼的空間復雜度為O(NW),復雜度較高。觀察狀態轉移方程可以得出:dp[i][j]只與第i-1行的狀態量有關,因此我們可以縮減掉一維,使得空間復雜度為O(W)。

int knapsack(vector<int> Values,vector<int> Weight,int W,int N){
    vector<int> dp(W+1,0);
    for(int i=1;i<=N;i++){
        for(int j=W;j>=Weight[i-1];j++){
            dp[j]=max(dp[j],dp[j-Weight[i-1]]+Values[i-1]);
    }
    return dp [W];
}

注意:在第二層循環中,我們是逆序遍歷,因為只有這樣我們才能獲得到第i-1行的dp[j-Weght[i-1]]數據。我們如果正序遍歷的話,在遍歷到j以前j-k處的數據已經從第i-1行的數據更新到了第i行的數據,使得結果出錯。

最后,再附上一道0/1背包問題的編程題:(題目來源:洛谷P1048)

#include<iostream>
#include<vector>
#include<math.h>
using namespace std;

int main() {
	int T, M;
	cin >> T >> M;
	int* time = new int[M];
	int* value = new int[M];
	for (int i = 0; i < M; i++) {
		cin >> time[i] >> value[i];
	}
	vector<int> dp(T + 1, 0);
	for(int i=1;i<=M;i++)
		for (int j = T; j > 0; j--) {
			if(j>=time[i-1])
				dp[j] = max(dp[j], dp[j - time[i - 1]]+value[i-1]);

		}
	cout << dp[T];
	delete[]time;
	delete[]value;
	return 0;
}

2022.05.01 補充:

要求裝滿的0/1背包問題

現在我們來考慮如果要求背包必須裝滿的情況之下我們所能拿到的最大價值。

這種情況與不做要求的0/1背包問題的差別其實在於初始化的不同:(以一維狀態舉例)

  在不要求裝滿的情況下,任何狀態都存在一個合法解dp[i]=0,表示在背包容量為i的情況下我們選擇不裝任何物品進入背包,總價值為零。然而加上背包必須裝滿的限制條件后,我們不一定能夠剛好取到總容量為i的物品集合,此時dp[i]為無效解。所以我們在初始化時將dp[0]初始化為0,因為此時0是一個合法解,其他情況均初始化為INT_MIN。我們再來分析一下動態規划的核心循環:

    for(int i=1;i<=N;i++){
        for(int j=W;j>=Weight[i-1];j++){
            dp[j]=max(dp[j],dp[j-Weight[i-1]]+Values[i-1]);

由於我們將無效解定為INT_MIN,從狀態轉移表達式我們不難看出,合法解只能由合法解推導出來,無法從非法解得到合法解。

舉個栗子,如果dp[j]為非法解,dp[j-Weight[i-1]]為合法解,那么dp[j]就會被更新為一個合法解。

同理,如果dp[j]為一個合法解,dp[j-Weight[i-1]]為一個非法解,那么dp[j]仍然保持為合法解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM