背包问题 模板详解!

——————————————————————————————————

一、01背包

题目描述:
N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

每件物品只能 不选0,或者选一次1,所以叫做01背包。(好生动 QwQ)


—————朴素做法:

1、状态表示:f[i][j]表示从前i个物品中选,总体积不超过j的最大价值;

2、属性: 最大值;

3、状态转移:
用“最后一步”来划分:当前物品“选不选”

  • 不选当前物品,那么当前状态f[i][j](从包括当前物品i的前面所有物品中选,总体积不超过j的最大价值)就为:从不包括当前物品的前面所有物品中选,总体积不超过j的最大价值。
    即:f[i-1][j];
  • 选当前物品,那么当前状态f[i][j]就为:从不包括当前物品i的前面所有物品中选,总体积不超过j-v[i](因为选了当前物品,要加上当前物品体积才到j,把当前位置空出来)的最大价值+当前物品的价值w[i]
    即:f[i-1][j-v[i]]+w[i];
    很容易看出这种情况只能在枚举的最大体积j大于等于当前物品体积v[i]时,才能更新当前状态;

所以,当前物品状态更新就为f[i][j]=f[i-1][j]+f[i-1][j-v[i]]+w[i];

4、时间复杂度: O(n*m),n为物品总数,m为背包最大体积。

#因为后面物品的更新需要用到前面物品的在某个总体积时的状态,所以要
(1)从前往后遍历所有物品;
(2)在该物品下,遍历总体积j
(3)之后再更新当前物品所在的总体积不超过j时的状态。

01背包模板(朴素做法):
#include<iostream>
using namespace std;

const int N=1010;
int n,sum;
int v[N],w[N];
int f[N][N];

int main(){
	cin>>n>>sum;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	//f[0][1~m],f[1][1~m],f[2][1~m]... 
	for(int i=1;i<=n;i++){
		for(int j=1;j<=sum;j++){
			if(j>=v[i]) //能装下当前物品 
				f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]); //不装当前物品和装当前物品,取最大 
			else f[i][j]=f[i-1][j]; //装不下当前物品就只能和前一个价值相同 
		}
	}
	cout<<f[n][sum];
	return 0;
} 

—————一维优化做法:

这种做法将状态表示从二维f[i][j]优化成了f[j],从而降低了空间复杂度,同时代码也更好实现。

二维优化将枚举物品位置的第一维去掉了,最后同样能够更新出n个物品能够装下的最大价值。

#为什么能优化成一维呢?

二维时的更新方式:f[i][j]=max(f[i - 1][j] ,f[i - 1][j - v[i]] + w[i]);
(1) 我们发现,对于每次循环的下一组i,只会用到i-1来更新当前值,不会用到i-2及之前值。于是可以在这次更新的时候,将原来的更新掉,反正以后也用不到。
所以对于i的更新,只需用一个数组,直接覆盖就行了。
(2) 我们发现,现在物品i时,对于每次j的更新,要用到之前i-1时的j或者j-v[i],不会用到后面的值。
所以为了防止串着改,我们将一维数组采取从后往前更新的方式,用原来i-1的数组来更新i之后,再将i覆盖掉。
(因为如果从前往后更新的话,前面的更新过之后,会接着更新后面的值,这样就不能保证是用原来i-1的数组来更新i的了)

这样,原来二维的状态表示就更新成一维了。#

分析一波:

1、状态表示:f[j]表示总体积不超过j时,能装下物品的最大价值;

2、属性: 最大值;

3、状态转移:
只用一个数组,每次都覆盖前面的数组。
当前物品“选不选”:

  • 不选当前物品的话,最大价值和前一位置的(原来i-1数组的这个位置上的值)是相同的,所以不用改变。

  • 选当前物品的话,需要和前一位置的信息(原来i-1数组的j-v[i]位置上值)取max。
    所以,更新方式就为:f[j]=max(f[j],f[j-v[i]]+w[i]);

整个更新方式就相当于:
每次i++,就从后往前覆盖一遍 f数组,看每个位置上的值是否更新。

4、时间复杂度: O(n*m),n为物品总数,m为背包最大体积。

01背包模板(一维优化):
#include<iostream>
using namespace std;

const int N=1010;
int n,m,v[N],w[N],f[N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++){
		for(int j=m;j>=v[i];j--){ //防止串着改,体积从后往前遍历。总体积只用遍历到v[i]就行,再小当前物品就装不下了,和这个位置原来的数一样,不用更新。
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}

注意遍历总体积j时只需要遍历到v[i]就行。
因为再小当前物品就放不下了,就更新不了了,和上一物品的当前总体积f[i-1][j]的状态一样(也就是优化后的f[j]),就是这一维数组当前位置上的值,所以不用更新。

—————————————————————————————————

二、完全背包:

题目描述:

N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

与01背包不同的是,完全背包模型中的是n种物品,每种物品有无限件。可以选无限次。


—————二维朴素做法:

每种物品可取任意个,所以要遍历每种物品拿了多少个。(当然不能超过背包容量)

1、状态表示:f[i][j]表示从前i个物品中选,总体积不超过j的最大价值;

2、属性: 最大值;

3、状态转移:
当前种类的物品,“选了多少个”:

  • 选0个,就是前面物品在当前体积下的最大价值:f[i-1][j]
  • 选1个,前面物品在去掉当前1个物品体积的最大价值+当前1个物品的价值:f[i-1][j-1*v[i]]+1*w[i];
  • 选2个,前面物品在去掉当前2个物品体积的最大价值+当前2个物品的价值:f[i-1][j-2*v[i]]+2*w[i];
  • … …
    最多能选多少个呢?
    选择物品的总体积k*v[i]不大于当前枚举的最大体积j

所以状态更新:f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i],k为选择的当前种类物品个数。k*当前物品体积<=当前枚举的背包容量;

4、时间复杂度: O(n·m·k),n为物品种类总数,m为背包最大体积,k为物品个数。

完全背包模板(二维朴素):
#include<iostream>
using namespace std;

const int N=1010;
int n,m,v[N],w[N],f[N][N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=0;k*v[i]<=j;k++) //k为拿的个数,从0开始,注意不能超过当前最大容积j  
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
			
	cout<<f[n][m];
	return 0;
}

—————二维一般做法:

二维一般做法将遍历一种物品选择个数的一层循环优化掉了,直接用前两重循环进行状态更新。这是如何做到的呢?让我们来看看吧:
推导过程:
f[i,j]f[i,j-v]两项展开:(为方便比对,f[i,j]代表f[i][j]vw代表v[i]w[i]
f[i,j]=Max(f[i−1,j], f[i−1,j−v]+w, f[i−1,j−2v]+2w , ... f[i−1,j−kv]+kw)
f[i,j−v]=Max(f[i−1,j−v], f[i−1,j−2v]+w, …, f[i−1,j−kv]+(k−1)w)
(注意:这里的k是背包装载的体积限制,客观因素限制了只能选k个,所以原来的最后一项f[i-1,j-(k+1)v]+sw就不能有了,二式就少了一项)
在这里插入图片描述我们发现:
一式f[i,j]的第二项一直到最后一项 和 二式f[i,j-v]只是相差了一个w

所以,f[i][j]完全可以用f[i][j-v]来更新,而用不到k了!即f[i][j]=max⁡(f[i−1,j],f[i,j−v]+w);

从而,状态转移就可以化成:f[i][j]=max⁡(f[i−1][j], f[i][j−v[i]]+w[i];

状态表示f[i][j]的含义不变。
时间复杂度降为:O(n*m);

完全背包模板(二维一般做法):
#include<iostream>
using namespace std;

const int N=1010;
int n,m,v[N],w[N],f[N][N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
			else f[i][j]=f[i-1][j];
		}
	}
	cout<<f[n][m];
	return 0;
}

—————一维优化做法:

可以发现,二维优化过的状态转移为:f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])
而01背包的状态转移为:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])
可以发现,这两个式子是很相似的,唯一不同的地方在f[i][j-v[i]

先来看个问题:

完全背包是用当前的物品j-v[i]体积时的状态,再更新当前体积j的状态。
01背包是用前一物品j-v[i]体积时的状态,更新当前物品体积j的状态。
01背包是可以用滚动数组优化成一维的,那完全背包可不可以呢?

答案是肯定的。
我们在将01背包优化成一维的时候,考虑过一种情况,才不得不将一维数组从后往前更新。
当时01背包更新方式是:用前一物品的状态来更新当前物品状态,所以如果把前面物品状态先改变(改变成了当前物品较小体积的状态)的话,当前物品的状态就没法更新了。
而现在完全背包的更新,正是用当前物品的较小体积的状态来更新当前较大体积的状态,那不就没有之前的顾虑了么?
这太棒啦! 直接从小到大更新吧

分析一波:

1、状态表示:f[j]表示总体积不超过j时,能装下物品的最大价值;

2、属性: 最大值;

3、状态转移: 只用一个数组,每次都覆盖前面的数组。

当前物品“选不选”:

  • 不选当前物品的话,最大价值和前一位置的(原来i-1数组的这个位置上的值)是相同的,所以不用改变。

  • 选当前物品的话,需要和当前物品较小体积时的信息(当前数组的j-v[i]位置上值)取max。
    所以,更新方式就为:f[j]=max(f[j],f[j-v[i]]+w[i]);

整个更新方式就相当于:
每次i++,就从前往后覆盖一遍 f数组,看每个位置上的值是否更新。

完全背包模板(一维优化):
#include<iostream>
using namespace std;

const int N=1010;
int n,m,v[N],w[N],f[N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	
	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=m;j++) //只需从v[i]开始遍历总体积,总体积再小当前物品放不下,不需更新。
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	
	cout<<f[m];
	return 0;
} 

和01背包一维优化后一样,总体积只需从v[i]开始遍历。
前面的就是上个物品的状态,也是当前位置的最佳状态(当前物品放不下,只能和上一物品的最大价值一样)。

———————————————————————————————

多重背包:

题目描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

和完全背包不同的是,这里每种物品有个数限制。并不是能无限拿的。


朴素做法:

每个物品有个数限制,那就像完全背包一样,枚举拿了多少个不就行了?

分析一波:

1、状态表示:f[i][j]表示从前i个物品中选,总体积不超过j的最大价值;

2、属性: 最大值;

3、状态转移:
当前种类的物品,“选了多少个”:

  • 选0个,就是前面物品在当前体积下的最大价值:f[i-1][j]
  • 选1个,前面物品在去掉当前1个物品体积的最大价值+当前1个物品的价值:f[i-1][j-1*v[i]]+1*w[i];
  • 选2个,前面物品在去掉当前2个物品体积的最大价值+当前2个物品的价值:f[i-1][j-2*v[i]]+2*w[i];
  • … …
    最多能选多少个呢?
    1、当前种类的物品有个数限制s[i];
    2、选择物品的总体积k*v[i]不大于当前枚举的最大体积j

    这两个条件都要满足。

状态更新:f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i],k为选择的当前种类物品个数。k<=当前种类的个数限制&&k*当前物品体积<=枚举的背包容量

4、时间复杂度: O(n·m·k),n为物品种类总数,m为背包最大体积,k为物品个数。

多重背包(朴素做法):
#include<iostream>
using namespace std;

const int N=110;
int n,m,v[N],w[N],f[N][N],s[N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
	
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=0;k<=s[i]&&k*v[i]<=j;k++) //个数限制,且不能超过当前总体积j. 
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);

	cout<<f[n][m];
	return 0;
} 

二进制优化:

将每种物品的个数限制用二进制划分,一种物品就被转化成若干个新的物品。这些物品的体积和价值互不相同,从而转化成01背包问题。

为何能如此转化呢?

用二进制数1,2,4,8,…能够将所有的数都表示出来.
将一种物品的个数si,划分成若干个分组(每组中物品个数为二进制数),这些分组能够组合成任意个该同种物品!
那拿原本同种的几个物品,也就相当于拿现在不同的几个分组。

一个分组就相当于一个新的物品

那“新物品”的体积和价值也随 其代表的旧物品个数k 而更新
新物品的体积: k*vi
新物品的价值: k*wi

k为这个新物品物品代表的原来的同种物品的个数,大部分为二进制数。
(因为一种物品的个数 最后如果不能完全划分成二进制的话,用最后划分剩下的个数当作k。只有最后一个可能不是二进制数。但是不影响,一样能用这些划分出的k组合出任意的个数)

所以,一种物品有si个,就可以转化(拆)成 log si 个新物品。
所以,总的新物品个数就为N*logs

这些新的物品,在组成任意个原来物品时,要么不选、要么选一次
于是就转化为01背包问题
用这些新的物品跑01背包就行啦!

多重背包模板(二进制优化):
#include<iostream>
using namespace std;

const int N=25000,M=2010;
int n,m,v[N],w[N],f[M];

int main(){
	cin>>n>>m;
	
	int cnt=0; //计数新物品个数 
	for(int i=1;i<=n;i++)
	{
		int x,y,s;
		cin>>x>>y>>s; //每种物品的体积,价值,个数 
		
		int k=1; //进制数初始为1 
		while(k<=s)
		{
			cnt++; //新物品个数++ 
			v[cnt]=k*x; //新物品体积为:其包含的单个物品的个数*单个物品体积 
			w[cnt]=k*y; //新物品价值为:个数*单个物品价值 
			s-=k; //个数-进制数 
			k*=2; //进制数更新 
		}
		if(s>0){ //进制数总和不能完全达到s,剩下的数再组成一个新物品 
			cnt++;
			v[cnt]=s*x;
			w[cnt]=s*y;
		}
	}
	
	n=cnt; //别忘了把物品总数更新为新物品个数. 
	for(int i=1;i<=n;i++){ //跑01背包,表示从新物品中挑几个,组成原来的物品 
		for(int j=m;j>=v[i];j--){
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
} 

这样,多重背包的时间复杂度就从之前的O(n·m·k)优化成了O(n·m·logs)


待更中。。

  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 多重背包问题是指在给定容量和物品的价值和重量的情况下,如何最大限度地装入物品,使得总价值最大化的问题。它的模板是:给定N种物品和一个容量为V的背包,每种物品有无限件可用,每件物品的重量是w[i],其价值是v[i]。求解将哪些物品装入背包可使价值总和最大。 ### 回答2: 多重背包问题是一个经典的组合优化问题,它是在0/1背包问题的基础上进行了扩展。在多重背包问题中,每个物品可以被选择的次数不再是1次,而是有一个确定的上限k次(k>1)。我们需要选择一些物品放入背包中,使得它们的总体积不超过背包的容量,并且使得它们的总价值最大化。 要解决多重背包问题,可以使用动态规划的方法。首先,我们定义一个二维数组dp[i][j],其中i表示前i个物品,j表示背包的容量。dp[i][j]表示当只考虑前i个物品、背包容量为j时,能够获取的最大价值。然后,我们可以使用如下的状态转移方程来计算dp[i][j]的值: dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2v[i]]+2w[i], ..., dp[i-1][j-kv[i]]+kw[i]) 其中,v[i]表示第i个物品的体积,w[i]表示第i个物品的价值,k表示第i个物品的可选次数。上述状态转移方程的意义是,我们可以选择不取第i个物品,或者分别取1次、2次、...、k次第i个物品,选择这些情况下的最大价值。 最后,我们可以通过遍历所有的物品和背包容量,计算出dp[n][m],其中n表示物品的个数,m表示背包的容量。dp[n][m]即为问题的解,表示只考虑前n个物品、背包容量为m时能够获取的最大价值。 综上所述,多重背包问题的解决方法是利用动态规划,通过定义状态转移方程和计算数组dp的值,找到问题的最优解。希望以上介绍对您有所帮助。 ### 回答3: 多重背包问题是常见的背包问题之一,与0-1背包问题和完全背包问题类似,但有一些区别。 在多重背包问题中,给定n个物品和一个容量为V的背包,每个物品有两个属性:重量w和价值v。同时,每个物品还有对应的个数限制c,表示该物品的数量最多可以选择c次。 我们需要选择物品放入背包,使得背包的总容量不超过V,同时物品的总价值最大。 多重背包问题可以用动态规划来解决。 我们可以定义一个二维数组dp,其中dp[i][j]表示前i个物品中选择若干个物品放入容量为j的背包时的最大价值。 根据多重背包问题的特点,我们需要对每个物品的个数进行遍历,并依次判断放入背包的个数是否超过c。 具体的状态转移方程为: dp[i][j] = max(dp[i-1][j-k*w[i]] + k*v[i]),其中0 <= k <= min(c[i], j/w[i]) 最后,需要注意的是多重背包问题的时间复杂度较高,为O(N*V*∑(c[i])),其中N是物品的数量,V是背包的容量,∑(c[i])表示物品的个数限制的总和。 总结而言,多重背包问题是在0-1背包问题和完全背包问题基础上的一种更复杂的情况,需要对每个物品的个数进行遍历和判断,采用动态规划求解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值