二维背包问题
问题:二维背包问题是指每件物品都具有两种不同的开销,选择这件物品必须同时承担这两种开销,在选择物品的时候必须保证两种开销都不会超过相应的背包容量,求选择物品可以得到最大的权重。设第i件物品所需的两种开销分别为v[i]和u[i],两种开销所对应的背包容量分别为V和U,物品的价值为w[i]。
分析:相比经典的01背包问题,二维背包问题增加了一维开销,于是我们需要在状态上增加一维。设s[i][j][k]表示将前i件物品放入两种容量分别为j和k的背包时所能获得的最大权重,则状态转移方程为s[i][j][k]=max{s[i-1][j][k], s[i-1][j-v[i]][k-u[i]]+w[i]},递推边界为当i=0时s[i][j][k]=0。和01背包类似,状态的维数可以很容易地从三维降低到二维,具体实现见代码。
代码:
for (int i=0; i<=V; i++)
{
for (int j=0; j<=U; j++) s[i][j]=0;
}
for (int i=1; i<=N; i++)
{
for (int j=V; j>=v[i]; j--)
{
for (int k=U; k>=u[i]; k--) s[j][k]=max(s[j][k], s[j-v[i]][k-u[i]]+w[i]);
}
}
二维背包的完全背包问题以及多重背包问题均与01背包类似,在此就不再赘述了。由二维背包问题推知多维背包问题可以通过继续增加状态维数的方法来解决。其他类型的DP问题如果是通过原型问题增加限制条件改编而来,应该也可以通过类似的增加状态维数的方法来解决。
问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
注意这里的三层循环的顺序,甚至在本文的第一个beta版中我自己都写错了。“for v=V..0”这一层循环必须在“for 所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。
另外,显然可以对每组内的物品应用P02中“一个简单有效的优化”。
小结
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如P07),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。
问题描述:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
所谓01背包,表示每一个物品只有一个,要么装入,要么不装入。
二 解决方案:
考虑使用dp问题 求解,定义一个递归式 opt[i][v] 表示前i个物品,在背包容量大小为v的情况下,最大的装载量。
opt[i][v] = max(opt[i-1][v] , opt[i-1][v-c[i]] + w[i])
解释如下:
opt[i-1][v] 表示第i件物品不装入背包中,而opt[i-1][v-c[i]] + w[i] 表示第i件物品装入背包中。
花费如下:
时间复杂度为o(V * T) ,空间复杂度为o(V * T) 。 时间复杂度已经无法优化,但是空间复杂度则可以进行优化。
但必须将V 递减的方式进行遍历,即V.......0 的方式进行。
三 初始化:
(1)若要求背包必须放满,则初始如下:
f[0] = 0 , f[1...V]表示-INF。表示当容积为0时,只接受一个容积为0的物品入包。
(2)若要求背包可以空下,则初始化如下:
f[0...V] = 0 ,表示任意容积的背包都有一个有效解即为0。
具体解释如下:
初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。
如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,
其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,
这个解的价值为0,所以初始时状态的值也就全部为0了。
四 代码如下:
/*
01背包,使用了优化后的存储空间
建立数组
f[i][v] = max(f[i-1][v] , f[i-1][v-c[i]] + w[i])
将前i件物品,放入容量为v的背包中的最大值。
下面介绍一个优化,使用一维数组,来表示
(1) f[v]表示每一种类型的物品,在容量为v的情况下,最大值。
但是体积循环的时候,需要从v----1循环递减。
初始化问题:
(1)若要求背包中不允许有剩余空间,则可以将f[0]均初始化为0,其余的f[1..n]均初始化为-INF 。
表示只有当容积为0 的时候,允许放入质量为0的物品。
而当容积不为0的情况下,不允许放入质量为0的物品,并且把状态置为未知状态。
(2)若要求背包中允许有剩余空间 ,则可以将f[1n],均初始化为0。
这样,当放不下去的时候,可以空着。
*/
#include
<
iostream
>
using
namespace
std ;
const
int
V
=
1000
;
//
总的体积
const
int
T
=
5
;
//
物品的种类
int
f[V
+
1
] ;
//
#define EMPTY
//
可以不装满
int
w[T]
=
{8 , 10 , 4 , 5 , 5}
;
//
价值
int
c[T]
=
{600 , 400 , 200 , 200 , 300}
;
//
每一个的体积
const
int
INF
=
-
66536
;
int
package()
{
#ifdef EMPTY
for(int i = 0 ; i <= V ;i++) //条件编译,表示背包可以不存储满
f[i] = 0 ;
#else
f[0] = 0 ;
for(int i = 1 ; i <= V ;i++)//条件编译,表示背包必须全部存储满
f[i] = INF ;
#endif
for(int i = 0 ; i < T ; i++)
{
for(int v = V ; v >= c[i] ;v--) //必须全部从V递减到0
{
f[v] = max(f[v-c[i]] + w[i] , f[v]) ; //此f[v]实质上是表示的是i-1次之前的值。
}
}
return f[V] ;
}
int
main()
{
int temp = package() ;
cout<<temp<<endl ;
system("pause") ;
return 0 ;
}
P02: 完全背包问题
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
这跟01背包问题一样有O(N*V)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的。
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。
一个简单有效的优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的O(N^2)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。这个不太重要的过程就不给出伪代码了,希望你能独立思考写出伪代码或程序。
转化为01背包问题求解
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
更高效的转化方法是:把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<=V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/c[i]))件物品,是一个很大的改进。
但我们有更优的O(VN)的算法。
O(VN)的算法
这个算法使用一维数组,先看伪代码:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-cost]+weight}
你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。
这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:
f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}
将这个方程用一维数组实现,便得到了上面的伪代码。
最后抽象出处理一件完全背包类物品的过程伪代码,以后会用到:
procedure CompletePack(cost,weight)
for v=cost..V
f[v]=max{f[v],f[v-c[i]]+w[i]}
总结
完全背包问题也是一个相当基础的背包问题,它有两个状态转移方程,分别在“基本思路”以及“O(VN)的算法“的小节中给出。希望你能够对这两个状态转移方程都仔细地体会,不仅记住,也要弄明白它们是怎么得出来的,最好能够自己想一种得到这些方程的方法。事实上,对每一道动态规划题目都思考其方程的意义以及如何得来,是加深对动态规划的理解、提高动态规划功力的好方法。
P03: 多重背包问题
题目
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则有状态转移方程:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}
复杂度是O(V*Σn[i])。
转化为01背包问题
另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解,复杂度仍然是O(V*Σn[i])。
但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。
方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出,并不难,希望你自己思考尝试一下。
这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题,是很大的改进。
下面给出O(log amount)时间处理一件多重背包中物品的过程,其中amount表示物品的数量:
procedure MultiplePack(cost,weight,amount)
if cost*amount>=V
CompletePack(cost,weight)
return
integer k=1
while k<num
ZeroOnePack(k*cost,k*weight)
amount=amount-k
k=k*2
ZeroOnePack(amount*cost,amount*weight)
希望你仔细体会这个伪代码,如果不太理解的话,不妨翻译成程序代码以后,单步执行几次,或者头脑加纸笔模拟一下,也许就会慢慢理解了。
O(VN)的算法
多重背包问题同样有O(VN)的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)的时间求解。由于用单调队列优化的DP已超出了NOIP的范围,故本文不再展开讲解。我最初了解到这个方法是在楼天成的“男人八题”幻灯片上。
01 背包
有n 种不同的物品,每个物品有两个属性,size 体积,value 价值,现在给一个容量为 w 的背包,问最多可带走多少价值的物品。
int f[w+1]; //f[x] 表示背包容量为x 时的最大价值
for (int i=0; i<n; i++)
for (int j=w; j>=size[i]; j--)
f[j] = max(f[j], f[j-size[i]]+value[i]);
完全背包
如果物品不计件数,就是每个物品不只一件的话,稍微改下即可
for (int i=0; i<n; i++)
for (int j=size[i]; j<=w; j++)
f[j] = max(f[j], f[j-size[i]]+value[i]);
f[w] 即为所求
初始化分两种情况:
1、如果背包要求正好装满则初始化 f[0] = 0, f[1~w] = -INF;
2、如果不需要正好装满 f[0~v] = 0;
举例:
01背包
V=10,N=3,c[]={3,4,5}, w={4,5,6}
(1)背包不一定装满
计算顺序是:从右往左,自上而下:因为每个物品只能放一次,前面的体积小的会影响体积大的
(2)背包刚好装满
计算顺序是:从右往左,自上而下。注意初始值,其中-inf表示负无穷
完全背包
:
V=10,N=3,c[]={3,4,5}, w={4,5,6}
(1)背包不一定装满
计算顺序是:从左往右,自上而下: 每个物品可以放多次,前面的会影响后面的
(2)背包刚好装满
计算顺序是:从左往右,自上而下。注意初始值,其中-inf表示负无穷
多重背包
:
多重背包问题要求很简单,就是每件物品给出确定的件数,求
可得到的最大价值
多重背包转换成 01 背包问题就是多了个初始化,把它的件数C 用二进制
分解成若干个件数的集合,这里面数字可以组合成任意小于等于C
的件数,而且不会重复,之所以叫二进制分解,是因为这样分解可
以用数字的二进制形式来解释
比如:7的二进制 7 = 111 它可以分解成 001 010 100 这三个数可以
组合成任意小于等于7 的数,而且每种组合都会得到不同的数
15 = 1111 可分解成 0001 0010 0100 1000 四个数字
如果13 = 1101 则分解为 0001 0010 0100 0110 前三个数字可以组合成
7以内任意一个数,即1、2、4可以组合为1——7内所有的数,加上 0110 = 6 可以组合成任意一个大于6 小于等于13
的数,比如12,可以让前面贡献6且后面也贡献6就行了。虽然有重复但总是能把 13 以内所有的数都考虑到了,基于这种
思想去把多件物品转换为,多种一件物品,就可用01 背包求解了。
看代码:
int n; //输入有多少种物品
int c; //每种物品有多少件
int v; //每种物品的价值
int s; //每种物品的尺寸
int count = 0; //分解后可得到多少种物品
int value[MAX]; //用来保存分解后的物品价值
int size[MAX]; //用来保存分解后物品体积
scanf("%d", &n); //先输入有多少种物品,接下来对每种物品进行分解
while (n--) { //接下来输入n中这个物品
scanf("%d%d%d", &c, &s, &v); //输入每种物品的数目和价值
for (int k=1; k<=c; k<<=1) { //<<右移 相当于乘二
value[count] = k*v;
size[count++] = k*s;
c -= k;
}
if (c > 0) {
value[count] = c*v;
size[count++] = c*s;
}
}
定理:一个正整数n可以被分解成1,2,4,…,2^(k-1),n-2^k+1(k是满足n-2^k+1>0的最大整数)的形式,且1~n之内的所有整数均可以唯一表示成1,2,4,…,2^(k-1),n-2^k+1中某几个数的和的形式。
证明如下:
(1) 数列1,2,4,…,2^(k-1),n-2^k+1中所有元素的和为n,所以若干元素的和的范围为:[1, n];
(2)如果正整数t<= 2^k – 1,则t一定能用1,2,4,…,2^(k-1)中某几个数的和表示,这个很容易证明:我们把t的二进制表示写出来,很明显,t可以表示成n=a0*2^0+a1*2^1+…+ak*2^(k-1),其中ak=0或者1,表示t的第ak位二进制数为0或者1.
(3)如果t>=2^k,设s=n-2^k+1,则t-s<=2^k-1,因而t-s可以表示成1,2,4,…,2^(k-1)中某几个数的和的形式,进而t可以表示成1,2,4,…,2^(k-1),s中某几个数的和(加数中一定含有s)的形式。
(证毕!)
现在用count 代替 n 就和01 背包问题完全一样了
杭电2191题解:此为多重背包用01和完全背包:
- #include<stdio.h>
- #include<string.h>
- int dp[102];
- int p[102],h[102],c[102];
- int n,m;
- void comback(int v,int w)//经费,重量。完全背包;
- {
- for(int i=v;i<=n;i++)
- if(dp[i]<dp[i-v]+w)
- dp[i]=dp[i-v]+w;
- }
- void oneback(int v,int w)//经费,重量;01背包;
- {
- for(int i=n;i>=v;i--)
- if(dp[i]<dp[i-v]+w)
- dp[i]=dp[i-v]+w;
- }
- int main()
- {
- int ncase,i,j,k;
- scanf("%d",&ncase);
- while(ncase--)
- { memset(dp,0,sizeof(dp));
- scanf("%d%d",&n,&m);//经费,种类;
- for(i=1;i<=m;i++)
- {
- scanf("%d%d%d",&p[i],&h[i],&c[i]);//价值,重量,数量;
- if(p[i]*c[i]>=n) comback(p[i],h[i]);
- else
- {
- for(j=1;j<c[i];j<<1)
- {
- oneback(j*p[i],j*h[i]);
- c[i]=c[i]-j;
- }
- oneback(p[i]*c[i],h[i]*c[i]);
- }
- }
- printf("%d\n",dp[n]);
- }
- return 0;
- }
只是用01背包,用二进制优化:
- #include <iostream>
- using namespace std;
- int main()
- {
- int nCase,Limit,nKind,i,j,k, v[111],w[111],c[111],dp[111];
- //v[]存价值,w[]存尺寸,c[]存件数
- //在本题中,价值是米的重量,尺寸是米的价格
- int count,Value[1111],size[1111];
- //count存储分解完后的物品总数
- //Value存储分解完后每件物品的价值
- //size存储分解完后每件物品的尺寸
- cin>>nCase;
- while(nCase--)
- {
- count=0;
- cin>>Limit>>nKind;
- for(i=0;i<nKind;i++)
- {
- cin>>w[i]>>v[i]>>c[i];
- //对该种类的c[i]件物品进行二进制分解
- for(j=1;j<=c[i];j<<=1)
- {
- //<<右移1位,相当于乘2
- Value[count]=j*v[i];
- size[count++]=j*w[i];
- c[i]-=j;
- }
- if(c[i]>0)
- {
- Value[count]=c[i]*v[i];
- size[count++]=c[i]*w[i];
- }
- }
- //经过上面对每一种物品的分解,
- //现在Value[]存的就是分解后的物品价值
- //size[]存的就是分解后的物品尺寸
- //count就相当于原来的n
- //下面就直接用01背包算法来解
- memset(dp,0,sizeof(dp));
- for(i=0;i<count;i++)
- for(j=Limit;j>=size[i];j--)
- if(dp[j]<dp[j-size[i]]+Value[i])
- dp[j]=dp[j-size[i]]+Value[i];
-
- cout<<dp[Limit]<<endl;
- }
- return 0;
- }
-
未优化的:
- #include<iostream>
- #include<cstdio>
- #include<cstring>
- using namespace std;
-
- int Value[105];
- int Cost[105];
- int Bag[105];
- int dp[105];
-
- int main()
- {
- int C,m,n;
- scanf("%d",&C);
- while(C--)
- {
- scanf("%d%d",&n,&m);
- for(int i = 1; i <= m; i++)
- scanf("%d%d%d",&Cost[i],&Value[i],&Bag[i]);
- memset(dp,0,sizeof(dp));
- for(int i=1;i<= m;i++)
- for(int j=1;j<=Bag[i];j++)
- for(int k=n;k>=Cost[i];k--)
- dp[k]=max(dp[k], dp[k-Cost[i]]+Value[i]);
- printf("%d\n",dp[n]);
- }
- return 0;
- }