最简单的背包问题——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]仍然保持为合法解。

posted @ 2022-02-23 22:07  天涯海角寻天涯  阅读(756)  评论(0编辑  收藏  举报