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;
}