01背包的变形问题----背包恰好装满

01背包的变形问题----背包恰好装满

在看本文之前建议先看一下我之前发过的01背包详解。
https://blog.csdn.net/Iseno_V/article/details/100001133

1. 问题的变形

在前面讲到的01背包问题中,现在我们把条件改为 “求当背包恰好装满时候取得的最大价值”。这样的问题其实本质上和原始的01背包问题区别不大,我们只需要做出一点小小的调整。
需要指出的是该问题其实可分为两个问题。
(1)背包能否恰好装满?
(2)如果能恰好装满,恰好装满时候的最大价值是多少?

2. 恰好装满时的处理方法

为了便于理解,这里给出一个用数组实现的01背包代码(空间复杂度未优化),和之前用vector容器实现的原理是一样的,

//01背包--不要求正好装满
#include <iostream>
using namespace std;
#define max(N1,N2) N1>N2?N1:N2
int main()
{
	int N, V;
	while (cin >> V >> N)//这里要求V<1000,N<100,否则后面的数组装不下
	{
		int v[100], w[100];//体积和价值
		int f[1000] = { 0 };//状态转移矩阵,这里将f数组全部初始化为0
		for (int i = 1; i <= N; i++)
		{
			cin >> v[i] >> w[i];//输入原始数据
		}
		for (int i = 1; i <= n; i++)
		{
			for (int j = V; j >= v[i]; j--)
			{
				f[j] = max(f[j], f[j - v[i]] + w[i]);
			}
		}
		cout << f[V] << endl;//输出结果
	}
	return 0;
}

如果看不懂上面的代码一定要去本文开头给出的链接去看一看01背包的基本原理。

2.1 有效状态和无效状态

在普通的01背包中,由于不要求恰好装满,我们采用的初始化方式为

int f[1000] = { 0 };//状态转移矩阵,这里将f数组全部初始化为0

这个初始化的方法意思是:对任意的一个状态,把当前的价值初始化为0,代表背包为空时所包含的物体价值为0 。即认为不管背包当前容量,背包是空的那么价值就是0,,没装满背包的状态都是有效的状态,我们称之为有效状态
当要求恰好装满时,我们就不可以这么做了,因为我们认为背包没恰好装满的话,当前背包的状态无效,即无效状态只有恰好装满时候才是有效状态
我们不妨定义当背包的状态为无效状态时,f[i]的值是负无穷,这样我们就可以从状态转移矩阵中区分出有效状态和无效状态了。
先不关心如何计算f,有了这个定义,对于恰好装满的01背包问题,我们只需要判断f[V]中的值是否是负无穷,就知道背包能否装满了。当f[V]中的值不是负无穷,我们接下来需要设计状态转移方程,使f[V]就是背包恰好装满时候的最大价值。

2.2 01背包的有效状态都是根据以前的有效状态推导出来的

首先观察上面的代码,转移方程为

f[j] = max(f[j], f[j - v[i]] + w[i]);

这段代码时经过时间复杂度优化后的代码,在优化前是

for (int i = 1; i <= n; i++)
{
	for (int j = V; j >= 0; j--)
	{
		if (j >= w[i])//如果背包装得下当前的物体
		{
			f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);
		}
		else//如果背包装不下当前物体
		{
			f[i][j] = f[i - 1][j];
		}
	}
}

通过前面01背包基本原理的讲解,我们知道,这两段代码是等效的。
f[i][j] 的值是由 f[i - 1][j],f[i - 1][j - w[i]] 来推导出来的,也就是 f[i][j] 的取值只与前面的状态有关系。换句话说,所有的状态都是从以前的状态来推导出来的。再换句话说,所有的有效状态是从之前的有效状态和无效状态推导出来的!!!
接下来是重点!!!
接下来是重点!!!
接下来是重点!!!

如果无效状态无法推出有效状态,我们就不必关心无效状态(背包没装满)时候,背包内物体的总价值。
因为背包恰好装满时候我们只关心有效状态,而无效状态又无法推出有效状态。
也就可以把所有无效状态时背包的总价值都设置为负无穷。

我们来证明下无效状态无法推出有效状态
对于下面的状态转移方程

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

(1)如果f[i - 1][j]和 f[i - 1][j - w[i]] 都是有效状态,那么f[i][j]是由有效状态推出的。

(2)如果f[i - 1][j]是无效状态, f[i - 1][j - w[i]] 是有效状态:
此时f[i][j]背包容量为j,f[i - 1][j - w[i]] 背包容量为j - w[i]且是恰好装满的。从f[i - 1][j - w[i]] 变换到f[i][j]相当于向一个已经装了总体积为j - w[i]物体的背包中放入一个体积为w[i]的物体,新的容量为j与当前背包容量j相同!!!
刚才我们说了无效状态可以看作负无穷,于是 f[i - 1][j - w[i]] + v[i]作为有效状态一定大于f[i - 1][j],所以可以得到新的有效状态f[i][j]。
所以此时f[i][j]是有效状态且是由有效状态f[i - 1][j - w[i]]推出的。

(3)如果f[i - 1][j]是有效状态, f[i - 1][j - w[i]] 是无效状态:
f[i - 1][j - w[i]] 是无效状态说明当前的背包内装的物体体积小于j - w[i],再放入一个体积为w[i]的物体后容量小于j,也就是说f[i - 1][j - w[i]] + v[i]也是无效状态。
此时因为f[i - 1][j]是有效状态,所以一定大于无效状态f[i - 1][j - w[i]] + v[i],则f[i][j]是一个由效状态f[i - 1][j]推导出的有效状态。
所以此时f[i][j]是有效状态且是由有效状态f[i - 1][j]推出的。

综上,只有有效状态可以推出有效状态。

2.3 无效状态的处理方法

由于c++中最小的负整型为16进制数 0x80000000,我们无法做到真正的负无穷。于是为了在计算无效状态时候依然采用同样的状态转移方程,我们需要用点小手段,即

#define INF 0x80000000
for (int j = V; j >= v[i]; j--)
{
	f[j] = max(f[j], f[j - v[i]] + w[i]);
	if (f[j] < 0)
		f[j] = INF;
}

如果算出来的是无效状态,强制赋值为0x80000000。
这样就相当于模拟了负无穷加上一个有限的正整数的计算过程,即负无穷加上任意有限正数还是负无穷。

3. 01背包恰好装满的代码实现

综上,我们发现,恰好装满与不是恰好装满只是在初始化过程上有差别而已。只需要在初始化时候把所有的无效状态初始化为负无穷就可以了。但是,f[0]需要初始化为0,因为背包容量为0时,装的物体体积为0是有效状态!

//01背包--恰好装满
#include <iostream>
using namespace std;
#define max(N1,N2) N1>N2?N1:N2
#define INF 0x80000000
int main()
{
	int V, N;
	while (cin >> V >> N)//输入背包容量和物体数
	{
		int v[1000], w[1000];
		int f[10000] ;
		//下面是初始化所有的无效状态
		for (int i = 0; i < 10000; i++)
		{
			f[i] = INF;
		}
		//f[0]是有效状态
		f[0] = 0;
		//输入每个物体的体积和价值
		for (int i = 1; i <= N; i++)
		{
			cin >> v[i] >> w[i];
		}
		//动态规划过程
		for (int i = 1; i <= N; i++)
		{
			for (int j = V; j >= v[i]; j--)
			{
				f[j] = max(f[j], f[j - v[i]] + w[i]);
				if (f[j] < 0)
					f[j] = INF;
			}
		}
		//判断能否恰好装满背包
		if (f[V] > 0)
		{
			cout << f[V] << endl;//背包恰好装满了,输出结果
		}
		else
		{
			cout << "error" << endl;//背包不能恰好装满
		}
	}
	return 0;
}
  • 46
    点赞
  • 131
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值