浅说背包问题(中)

多重背包

有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 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]);
			}
		}
	}

虽然我们将顺序挑换了,但是可以发现 d p [ i ] [ j ] dp[i][j] dp[i][j]还是没有保存数据,因为它是按照 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ m [ i ] dp[i-1][j-k*v[i]]+k*m[i] dp[i1][jkv[i]]+km[i]来进行动态规划的,和 d p [ i ] [ j ] dp[i][j] dp[i][j]无关,所以我们这里要稍微改一改代码,把 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1]改成 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--){
			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]);
			}
		}
	}

那么我们似乎可以把它改成一维的滚动数组诶

	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 N2000,V2000

虽然物品有无限件,但是由于背包的体积有限,每种物品最多可以放的数量是有限的,即最多放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 v1v2v3v4
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 m103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ 1 0 4 1 \leq m \le 10^4 1m104 1 ≤ t ≤ 1 0 7 1 \leq t \leq 10^7 1t107,且 1 ≤ m × t ≤ 1 0 7 1 \leq m \times t \leq 10^7 1m×t107 1 ≤ a i , b i ≤ 1 0 4 1 \leq a_i, b_i \leq 10^4 1ai,bi104

不难发现,这就是一道最最最简单的完全背包问题,只不过请注意,需要开 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小时内回话
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值