多重背包
有N种物品和一个容量为V的背包。每种物品都有一个体积和价值,以及该物品有多少件。求解怎么装可使这些物品的体积总和不超过背包容量,且价值总和最大。问最大价值是多少
输入:
10 4
3 4 3
4 5 3
4 3 1
5 4 4
输出:
13
N<=2000,V<=20000.
这种背包问题和前面的背包问题不同的地方在于,一种物品可以有很多件,并不是只可以取一件。我们可以把它转换成01背包来求解。每种物品有m[i]件,可以理解为有m[i]种该物品,每种物品只有一件。这就转换成了01背包。
但是我们可以注意到这里会去枚举一个数量k,那么这个地方就有问题了,不难发现,k有两个位置可以放如图所示
for (int i=1;i<=n;i++){
for (int k1;;){//位置1
for (int j=1;j<=m;j++){
for (int k2;;){//位置2
}
}
}
}
那么我们就要仔细想想这里的问题了
位置1
如果放在位置1,那么我们就会得到以下代码
for (int i=1;i<=n;i++){
for (int k=0;k<=c[i];k++){
for (int j=m;j>=k*v[i];j--){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*m[i]);
}
}
}
来分析以下这段代码,不难发现dp[i][j]
似乎确凿被更新了许多次,但是都没有被保存下来,只有在k=c[i]的时候才有答案被保存了下来,所以这样是不行的
那么这时候就有人说了,那我们把他开成一维的滚动数组不久完了?好,我们来试试
for (int i=1;i<=n;i++){
for (int k=0;k<=c[i];k++){
for (int j=m;j>=k*v[i];j--){
dp[j]=max(dp[j],dp[j-k*v[i]]+k*m[i]);
}
}
}
不难发现,dp[i]
确实被持续更新了,但是,我们要注意一下,我们是按照01背包的方法来进行的,01背包的物品有一个特点,就是每个物品是独立的,是不会相互影响的,那么我们这里这样写了之后,就会发现我们其实是把选零个物品,选一个物品,选两个物品等看成了一个个单一的物品,那么这么说,我们在极端情况下是可以都选的,也就是说会选
c
[
i
]
×
(
c
[
i
]
−
1
)
2
\frac{c[i]\times(c[i]-1)}{2}
2c[i]×(c[i]−1)个,那么这样算出来的答案就一定是错的了,而且会大上不少。
位置2
如果我们放在位置二,就一定是对了吗?我们先写一个代码来看看
for (int i=1;i<=n;i++){
for (int j=m;j>=0;j--){
for (int k=0;k<=c[i];k++){
if (j<k*v[i])break;
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*m[i]);
}
}
}
虽然我们将顺序挑换了,但是可以发现 d p [ i ] [ j ] dp[i][j] dp[i][j]还是没有保存数据,因为它是按照 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]和 d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ m [ i ] dp[i-1][j-k*v[i]]+k*m[i] dp[i−1][j−k∗v[i]]+k∗m[i]来进行动态规划的,和 d p [ i ] [ j ] dp[i][j] dp[i][j]无关,所以我们这里要稍微改一改代码,把 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1]改成 d p [ i ] [ j ] dp[i][j] dp[i][j],这样我们就可以持续更新了,只不过以防万一,我们还是先对 d p [ i ] [ j ] dp[i][j] dp[i][j]附上值,如下
for (int i=1;i<=n;i++){
for (int j=m;j>=0;j--){
dp[i][j]=dp[i-1][j];
for (int k=0;k<=c[i];k++){
if (j<k*v[i])break;
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*m[i]);
}
}
}
那么我们似乎可以把它改成一维的滚动数组诶
for (int i=1;i<=n;i++){
for (int j=m;j>=0;j--){
for (int k=0;k<=c[i];k++){
if (j<k*v[i])break;
dp[j]=max(dp[j],dp[j-k*v[i]]+k*m[i]);
}
}
}
二进制优化
根据代码可知,该题的时间复杂度为
O
(
V
×
N
)
O(V\times N)
O(V×N)(N 表示所有物品的总数量)),如果每一种物品的数 量比较多,该题就容易超时,所以我们需要想办法优化一下。
对于每一种物品,可以选择的范围是[1,k], 暴力的做法是依次枚举物品选择0,1…k的情况, 但是这样枚举很冗余,存在重复枚举的情况。
比如枚举了1,2,表示有两种该物品,当把这两种物品都选择时,相当于就是选择该物品3个时 的情况,后面枚举的3就重复了,同样,后面的很多数字都可以通过前面枚举过的数字组合而成。
那么对于[1,k],怎么用最少的数表示出来这个区间内的所有数呢?
首先,1肯定是要有的。
其次,只有一个1,最大表示的数是1,没有办法表示出来2,所以2也需要。
3:可以通过1+2组合得到,没有要枚举。
4:1和2最多可以表示3,4是需要的。
5:1+4=5
6:2+4=6
7:1+2+4=7
8:(1,2,4)最多表示7, 8是需要的。 以此类推……
我们可以从中找到规律,只需要1,2,4,8…等2次方数就可以表示出来区间[1,k]之间的数。那么最后一个二进制数是多少呢?比如k=12如果最后一个二进制数是16,那么久表示这个物品我最多可以选16个,和题意不符。如果最后一个二进制数是8,[1,2,4,8],最大可以表示的数是15,但是实际最多可以选12个,也和题意不符。但是只有[1,2,4]没办法表示出来[8,12]。所以数应该是[1,2,4,5],虽然有的数可能会重复表示,但是这样表示的数的范围不会扩大,也不会遗漏。
代码如下:
int t=1;
while (c>=t){
tot++;
v[tot]=b*t;
w[tot]=a*t;
c-=t;
t*=2;
}
if (c>0){
tot++;
v[tot]=b*c;
w[tot]=a*c;
}
那么我们这样优化完了之后,就可以直接用01背包的方法求解啦
完整代码
#include<bits/stdc++.h>
using namespace std;
int tot=0,v[100000],w[100000],dp[20010];
int main(){
int m,n;
cin>>m>>n;
for (int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
int t=1;
while (c>=t){
tot++;
v[tot]=b*t;
w[tot]=a*t;
c-=t;
t*=2;
}
if (c>0){
tot++;
v[tot]=b*c;
w[tot]=a*c;
}
}
for (int i=1;i<=tot;i++){
for (int j=m;j>=w[i];j--){//标准的01背包
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[m];
return 0;
}
例题
[NOIP1996 提高组] 砝码称重
题目描述
设有 1 g 1\mathrm{g} 1g、 2 g 2\mathrm{g} 2g、 3 g 3\mathrm{g} 3g、 5 g 5\mathrm{g} 5g、 10 g 10\mathrm{g} 10g、 20 g 20\mathrm{g} 20g 的砝码各若干枚(其总重 $ \le 1000$),可以表示成多少种重量?
输入格式
输入方式: a 1 , a 2 , a 3 , a 4 , a 5 , a 6 a_1 , a_2 ,a_3 , a_4 , a_5 ,a_6 a1,a2,a3,a4,a5,a6
(表示 1 g 1\mathrm{g} 1g 砝码有 a 1 a_1 a1 个, 2 g 2\mathrm{g} 2g 砝码有 a 2 a_2 a2 个, … \dots …, 20 g 20\mathrm{g} 20g 砝码有 a 6 a_6 a6 个)
输出格式
输出方式:Total=N
( N N N 表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)
样例 #1
样例输入 #1
1 1 0 0 0 0
样例输出 #1
Total=3
提示
【题目来源】
NOIP 1996 提高组第四题
该题很容易想到爆搜,但是数据范围比较大,很容易超时,我们可以考虑其他做法。所有的砝码可以表示的最大数是砝码总重量之和,可以表示的最小的数是1。但是这里面哪些数是可以表示出来的呢?我们可以把题意转换成:有一个体积为V的背包,有n种不同的物品,是否刚好可以填满。即可以选择的物品的最大体积刚好等于V,那么就算一种方法,否则就不算一种方法,最后统计有多少种符合条件的情况。
完全背包
有N种物品和一个容量为V的背包。每种物品可以取无限次,每种物品有一个体积和价值。求解怎么装可使这些物品的体积总和不超过背包容量,且价值总和最大。 完全背包特点:每一种物品都可以取无限次且不可分割。
输入:
10 4
2 1
3 3
4 5
7 9
输出:
12
N ≤ 2000 , V ≤ 2000 N\le2000,V\le2000 N≤2000,V≤2000
虽然物品有无限件,但是由于背包的体积有限,每种物品最多可以放的数量是有限的,即最多放V/c[i]件,这就转换成了多重背包,然后再用二进制优化转换成01背包即可。
但是这样写,时间复杂度也要超,所以我们再来谈谈优化的方法
优化
优化办法一
因为物品有无限件,所以对于任意的两种物品,如果w[i]<w[j]并且c[i]>c[j],那么就说明i这种物品是肯定不可以取的。这样就可以筛选出很多不合理的物品。只不过这种的优化方法所优化的幅度不大,所以我们只做了解
优化办法二(重点)(敲黑板)
完全背包的写法和01背包的写法有点类似,只是内层循环的时候,01背包是[V,0],而完全背包是[0,V].前面我们已经探讨过了。如果用一维数组来表示01背包,内循环只能是[V,0],因为对于当物品种类为i的时候v的值,所需要的f[v-c[i]]的值实际上是当物品种类为i-1的时候的未更新的值,因为v>v-c[i]。如果循环写出[0,V],那么第一次更新了f[v1]的值,第二次求f[v2]的值的时候,假设v2-c[i]=v1.实际上是已经取过一次第i件物品的时候的f[v1]的值了,这和01背包不相符合。但是这刚好符合完全背包的要求。没一件物品可以取多次,直到背包放不下为止。
内循环的范围是[0,V],所以我们求值的顺序是
v
1
→
v
2
→
v
3
→
v
4
v1 \rightarrow v2 \rightarrow v3\rightarrow v4
v1→v2→v3→v4
f[v1]:取第i件物品一次时的值.(f[v1]值已经更新)
v2=v1+c[i];
f[v2]=max(f[v2c[i]]+w[i],f[v2])=max(f[v1]+w[i],f[v2]),因为在计算f[v2]之前就已经计算过f[v1]了,所以该表达式表示的是取第i件物品两次时的价值
v3=v2+c[i];
f[v3]=max(f[v2-c[i]]+w[i],f[v2])=max(f[v2]+w[i],f[v3]);在计算f[v3]之前就已经计算过f[v2]了,所以f[v2]表示取两次第i件物品的时候的最大价值,那么该表达时就是表示取取第i件物品三次时的价值
v4=v3+c[i];
也是也前面相同。所以,对于每一个i,随着v值的不断增加,实际上里面就包含了取第i件物品的时候的所有情况了。所以直接O(V*N)就搞定了。
代码如下
for(int i=1;i<=n;i++){
for(int v=c[i];v<=V;v++){
f[v]=max(f[v-c[i]]+w[i],f[v]);
}
}
例题
疯狂的采药
题目背景
此题为纪念 LiYuxiang 而生。
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
1 1 1. 每种草药可以无限制地疯狂采摘。
2 2 2. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 t t t 和代表山洞里的草药的数目 m m m。
第 2 2 2 到第 ( m + 1 ) (m + 1) (m+1) 行,每行两个整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数 a i , b i a_i, b_i ai,bi 分别表示采摘第 i i i 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例 #1
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
140
提示
数据规模与约定
- 对于 30 % 30\% 30% 的数据,保证 m ≤ 1 0 3 m \le 10^3 m≤103 。
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ 1 0 4 1 \leq m \le 10^4 1≤m≤104, 1 ≤ t ≤ 1 0 7 1 \leq t \leq 10^7 1≤t≤107,且 1 ≤ m × t ≤ 1 0 7 1 \leq m \times t \leq 10^7 1≤m×t≤107, 1 ≤ a i , b i ≤ 1 0 4 1 \leq a_i, b_i \leq 10^4 1≤ai,bi≤104。
不难发现,这就是一道最最最简单的完全背包问题,只不过请注意,需要开
l
o
n
g
l
o
n
g
long long
longlong,否则见祖宗
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5,M=1e7+5;
int n,m,w[N],v[N],f[M];
int main(){
scanf("%lld%lld",&m,&n);
for(int i=1;i<=n;i++)
scanf("%lld%lld",&w[i],&v[i]);
for(int i=1;i<=n;i++)
for(int j=w[i];j<=m;j++)
f[j]=max(f[j],f[j-w[i]]+v[i]);
printf("%lld",f[m]);
return 0;
}
注:此处借鉴了一下⚡小林子⚡的代码
混合背包
有n种物品,里面有的物品只可以取一次,有的物品可以取m次,有的物品可以取无限次。求最大价值是多少经过前面的学习,我们会发现,多重背包可以用二进制优化直接转换成01背包。写法完全一样。所以当只含有多重背包和01背包时直接转换成01背包求解。但是完全背包写法和01背包是相反的。所以就分情况讨论。
#include <bits/stdc++.h>
using namespace std;
int m, n;
int w[31], c[31], p[31];
int f[201];
int main(){
cin>>m>>n;
for (int i = 1; i <= n; i++){
cin>>w[i]>>c[i]>>p[i];
}
for (int i = 1; i <= n; i++){
if (p[i] == 0){
for(int j = w[i]; j <= m; j++){//完全背包
f[j] = max(f[j], f[j-w[i]]+c[i]);
}
}else{
for(int j = 1; j <= p[i]; j++){ //01背包和多重背包
for (int k = m; k >= w[i]; k--){
f[k] = max(f[k],f[k-w[i]]+c[i]);
}
}
}
}
cout<<f[m];
return 0;
}
但是这样写出来的代码的时间复杂度可能会比较高,所以我们可以在多重背包问题的时候,用二进制优化一下,这样写出来的代码就不会超时了
#include<bits/stdc++.h>
using namespace std;
int m,n;
int dp[205];
int v[135];
int w[135];
int p[135];
int cnt=0;
int main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++){
int t1,t2,t3;
scanf("%d%d%d",&t1,&t2,&t3);
if(t3!=0){
int t=1,sum=0;
while(sum+t<=t3){
cnt++;
v[cnt]=t1*t;
w[cnt]=t2*t;
p[cnt]=1;
sum+=t;
t*=2;
}
if(sum<t3){
cnt++;
v[cnt]=t1*(t3-sum);
w[cnt]=t2*(t3-sum);
p[cnt]=1;
}
}else{
cnt++;
v[cnt]=t1;
w[cnt]=t2;
p[cnt]=0;
}
}
for(int i=1;i<=cnt;i++){
if(p[i]==0){
for(int j=v[i];j<=m;j++){
dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
}
}else{
for(int j=m;j>=v[i];j--){
dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
}
}
}
cout<<dp[m]
return 0;
}
蛙趣,我这篇文章居然写了7000个字,破纪录了~~,所以赶快把你们手中的赞送出来吧!如果有不懂得,欢迎私聊,博主绝对在24小时内回话