01背包

13 篇文章 0 订阅
12 篇文章 0 订阅

01背包

Description

一个旅行者有一个最多能装 M 公斤的背包,现在有 n 件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn,求旅行者能获得最大总价值。

Input

第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);

第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。

Output

仅一行,一个数,表示最大总价值。


        于是我们可以用一个二维数组dp[n][m]来存储状态。dp[i][j]就表示当背包容量为j时对第i个物品进行决策的最优值。决策就是选与不选。若是不选的话dp[i][j]就可以由dp[i - 1][j]更新而来;若是选的话,则需要给第i个物品腾出空间,所以dp[i][j]就由dp[i - 1][j - w[i]]更新而来。于是就有了状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]])。最后的答案便是dp[n][m]。

        不过还有一个小细节需要处理:若当前背包容量不足以装下第i个物品时,则不选,此时dp[i][j]就由dp[i - 1][j]更新而来。

        于是这题便有了代码:

#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn], c[maxn];
int n, m;
int dp[maxn][maxm];

int main(){
    cin >> m >> n;
    for(int i = 1; i <= n; i++)
        cin >> w[i] >> c[i];
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            if(j >= w[i]){
            	dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
	    }else{
		dp[i][j] = dp[i - 1][j];
	    }

    cout << dp[n][m] << endl;
    return 0;
}

        不过认真思考之后不难得出,对第i个物品做决策时只需要由第i-1个物品更新而来,所以存储第1~i-2个物品的决策的数组就浪费掉了,因为每次做决策只需要两层数组。因此我们可以尝试用滚动数组来进行空间的优化:

#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn], c[maxn];
int n, m;
int dp[2][maxm];

int main(){
	cin >> m >> n;
	for(int i = 1; i <= n; i++)
		cin >> w[i] >> c[i];
	
	int p = 0;
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			if(j >= w[i]){
				dp[p][j] = max(dp[1 - p][j], dp[1 - p][j - w[i]] + c[i]);
			}else{
				dp[p][j] = dp[1 - p][j];
			}
		}
		p = 1 - p;//如果p=1,1-p可以得到0;如果p=0,1-p可以得到1
	}
	cout << dp[1 - p][m] << endl;//最后一层循环对p多操作了一次,所以退回去
	
	return 0;
}

        但是我们现在可不可以多这样一个操作:当前一层数组更新完后,将当前的数组复制到下一层数组,然后下一层数组以自己为基础进行更新呢?貌似是可以的:

        但如果这个物品的价值足够大,会发生什么呢?

        如图,这样的话一个物品就被选择了两次或以上,这是违背了题意的.不过真的不能够让数组以自己为基础更新自己吗?并不.由于dp[i][j]必须由之前的数据更新而来,所以我们把dp[i][j]之前的东西放到最后更新就好了嘛.即倒序循环.然后动态数组复制到下一层的操作干脆就可以省略了,直接用一维数组:

#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn], c[maxn];
int m, n;
int dp[maxm];

int main(){
	cin >> m >> n;
	for(int i = 1; i <= n; i++)
		cin >> w[i] >> c[i];
	
	for(int i = 1; i <= n; i++)
		for(int j = m; j >= w[i]; j--)
			dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
	
	cout << dp[m] << endl;
	
	return 0;
}

        截止现在,我们空间上已经在做了非常大的优化了:从一开始O(NM)的二维数组到都来的O(2M)的滚动数组,再到现在的O(M)的一位数组,简直优化了不要太多.但时间上是否也可以优化呢?

        显然是可以的.假设我们现在已经对第i-1个物品进行了决策,并且背包的总容量足够大.那么,对于剩下的第i~n个物品的决策最多需要多少空间呢?也就是全部选的情况,就需要∑(i, n)wi的容量.那么,我们此时就只需要更新数组的第m-∑(i,n)wi位到第m位就可以了,不必再更新第wi到第m-∑(i,n)wi位省去了几次循环.不过这里有个前提,就是背包容量足够大,即m-∑(i,n)wi>wi.下面给出参考代码:

#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn], c[maxn];
int m, n;
int dp[maxm], sum[maxn];

int main(){
	cin >> m >> n;
	for(int i = 1; i <= n; i++)
		cin >> w[i] >> c[i];
	
	for(int i = n; i >= 1; i--){
		sum[i] = sum[i + 1] + w[i];
	}//求和.sum[i]表示第i个物品到第n个物品的重量和.倒着加可以一边循环过
	
	int tmp;
	for(int i = 1; i <= n; i++){
		tmp = max(m - sum[i], w[i]);//循环之前先确定下限,否则循环时每次都会求一遍
		for(int j = m; j >= tmp; j--)
			dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
	}
	cout << dp[m] << endl;
	
	return 0;
}

美滋滋~

01背包就讲解到这里下面是一些01背包的变式:


1.背包是否能够装满

        由于这题不涉及物品价值,所以可以将dp数组定义为bool类型.dp[j]=true表示容量为j的背包能够被装满.于是,如果容量为j-w[i]的背包能够装满,那么容量为j的背包也就能够被装满.我们只需在循环之前把dp[0]初始化成true就可以了.下面给出参考代码:

#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn];
int m, n;
bool dp[maxm];

int main(){
	cin >> m >> n;
	for(int i = 1; i <= n; i++)
		cin >> w[i];
	
	dp[0] = true;
	for(int i = 1; i <= n; i++)
		for(int j = m; j >= w[i]; j--) if(dp[j - w[i]])
			dp[j] = true;
	cout << (dp[m] ? "yes" : "no") << endl;
	
	return 0;
} 

2.找方案.即最大价值是由哪些物品得来的

        首先这是01背包的变式,所以01背包的代码还是照常打上去.既然要找出方案,那我们就需要从所有物品的决策中推导.而之前优化出的一位数组虽然省了空间,却丢失了之前做的决策.所以我们这里只能使用二维数组.那么我们尝试从dp[1][0]开始推导,却发现缺少信息.所以很显然我们需要倒着推,从dp[n][m]开始.那么在容量为m的背包中对第n个物品进行决策,假如决策是"选",那么就将n存储起来,然后问题转变为容量为m-w[i]的背包里对n-1个物品进行决策,以此类推;假如决策时"不选",那么问题就转变为了在容量为m的背包里,对第n-1个物品进行决策.最后把存储的内容输出就行了.

        不过还有些小细节需要处理:

        (1)怎么存答案?--------显然,我们需要对最先存储的答案最后输出,所以可以用一个栈(stack)来存储.

        (2)如果有多种方案,要求输出字典序最小的怎么办?--------由于我们是倒着推导,所以我们对于第i个物品应该尽量地不选为什么呢?如果选了,那么答案里就会有i;而不选的话就会从第i个物品之前的物品进行推导,它们的字典序明显是小于i的.

于是就有了代码:

#include <iostream>
#include <stack>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;

int w[maxn], c[maxn];
int m, n;
int dp[maxn][maxm];
stack<int> ans;

int main(){
	cin >> m >> n;
	for(int i = 1; i <= n; i++)
		cin >> w[i] >> c[i];
	
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			if(j >= w[i]){
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
			}else{
				dp[i][j] = dp[i - 1][j];
			}
	
	for(int i = n; i >= 1; i--)
		if(dp[i][m] != dp[i - 1][m]){
			ans.push(i);
			m -= w[i];
		}
	while(!ans.empty()){
		cout << ans.top() << " ";
		ans.pop();
	}
	
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值