背包问题专题

背包问题是一类非常经典的动态规划问题,其主要表现为要在一定体积(或是一定时间之类的)要求下拿一定物品,求最多的价值,有着多种形式,这里来讲几种常见的背包问题

01背包

题目

将 N 件价值 v[ i ],体积为 c[ i ]的物品放入体积为 V 的背包中,如何使价值最大
<例题>

方法一

基本思路

01背包是背包问题最基础的形态,对于每件物品都只有放或不放两种情况
在这里定义 f[ i ][ j ],表示前 i 件物品占用 j 的容量(这里的占用并不一定是全装满)所得到的最大价值
故得状态转移方程式如下:

f [ i ][ j ]=max( f[ i - 1 ][ j ] ,f[ i - 1 ][ j - c[ i ] ] + v[ i ] )

即判断第 i 件物品是否要放在占用 j 的容量的地方时,只需考虑第 i - 1 件物品放完后的状态。
在 j 不放第 i 个物品时价值为最大f[ i - 1 ][ j ],在 j 放第 i 个物品时最大价值为f[ i - 1 ][ j - c[ i ] ] + v[ i ](将物品放入占空间 j-c[ i ] 的情况,用那种情况下的最大价值加上第 i 个物品的价值)。因此前 i 个物体的最大价值就是二者的最大值

代码实现
#include<iostream>
using namespace std;
const int N=1e5;
int c[N],v[N];
int f[N][N];
int main()
{
	int i,j,k,n,m;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i];
	for(i=1;i<=n;i++)
	{
	 	for(j=1;j<=m;j++)
		{
			if(j>=c[i])//如果空间不够放入物体,f[i][j]=f[i-1][j]
	 		{
				f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+v[i]);
	 		}
	 		else
	 		{
				f[i][j]=f[i-1][j];
			}
	 	}
	}
	cout<<f[n][m];
	return 0;
} 

方法二(空间优化)

基本思路

上一种方法的时间复杂度基本上已经达到最优,但在空间上仍旧有待优化
因为 f 数组每次更新都只需考虑上一轮更新的结果,所以我们可以将 f 数组缩减到一维,即根据自己更新自己
此时的状态转移方程式:

f [ j ] =max( f [ j ],f [ j - c [ i ] ] + v [ i ] )

这时的核心代码为:

for(i=1;i<=n;i++)
	for(j=m;j>=v[i];j--)
		f[j]=max(f[j],f[j-v[i]]+w[i]);

注意,自我更新时要采用倒序的方式,防止当前更新所用到的值是这一轮所更新过的,这点将在后文中详细讲到
(在这里

代码实现
#include<iostream>
using namespace std;
const int N=1e5;
int c[N],v[N];
int f[N];
int main()
{
	int i,j,k,n,m;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i];
	for(i=1;i<=n;i++)
	{
		for(j=m;j>=c[i];j--)//一定要注意是倒序!!!!
		{
			f[j]=max(f[j],f[j-c[i]]+v[i]);
		}
	}
	cout<<f[m];
	return 0;
} 

完全背包

题目

与01背包基本相同,只不过每件物品有无穷多个
<例题>

方法一

基本思路

因为物品有无穷多个,所以物品可以取0个、1个、2个…,只要总体积不超过 v 就可以一直放下去。
仍按照01背包的第一种思路,得状态转移方程式:

f [ i ][ j ]=max( f [ i - 1 ][ j ],f [ i - 1 ] [ j - k *c [ i ] ] + k *v [ i ]) || ( 0 ≤ k *c [ i ] ≤ j )

由此得具体更新操作:

for(i=1;i<=n;i++)
	for(j=1;j<=m;j++)
		for(k=0;k<=j/v[i];k++)
			f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); 

显然,这种方式的时间复杂度过高,仍有待简化

代码实现
#include<iostream>
using namespace std;
int n,m;
int c[1005],v[1005],f[1005][1005];
int main()
{
	int i,j,k;
	cin>>m>>n;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i];
	for(i=1;i<=n;i++)
	{
		for(j=1;j<=m;j++)
		{
			for(k=0;k<=j/c[i];k++)
			{
				f[i][j]=max(f[i][j],f[i-1][j-k*c[i]]+k*v[i]); 
			}
		}
	}
	cout<<f[n][m];
	return 0;
}

方法二(时间、空间优化)

基本思路

先前在01背包的空间优化中提到,01背包用倒序更新是为了防止更新所用到的值是被更新过的,而完全背包正好可以利用正序会更新被更新过的值这一特点,去更新已经更新过的值,从而达到对原来的代码进行优化的目的
状态转移方程式如下:

f [ j ] = max( f [ j ],f [ j - v [ i ] ] + w [ i ] );

更新操作转变为:

for(i=1;i<=n;i++)
{
	for(j=c[i];j<=m;j++)
	{
		f[j]=max(f[j],f[j-c[i]]+v[i]);
	}
}

这种方法能有效的将时间复杂度压缩至O(n*m),空间复杂度压缩至V(m)

代码实现
**#include<iostream>
using namespace std;
int n,m,c[1005],v[1005];
int f[1005];
int main()
{
	int i,j,k;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i];
	for(i=1;i<=n;i++)
	{
		for(j=c[i];j<=m;j++)//额外注意这里是正序,和01背包做好区分
		{
			f[j]=max(f[j],f[j-c[i]]+v[i]);
		}
	}
cout<<f[m];
return 0;
} **

其他的可用小技巧(代码示例中未使用)

因为多重背包的物品可以无限使用,所以当一个物品的体积大于另一个物品且价值小于那个物品时,体积大且价值小的物品可以被直接舍去,但在题内可能会被卡数据,可酌情使用

01背包与完全背包的更新顺序

当需要对背包进行更新时,背包的更新顺序将决定更新所用到的值是否被更新过
01背包更新时,因为其状态转移方程式更新所用到的是上一轮更新的结果,所以需要避免更新所用的值被更新而导致同一件物品被多次放置,故而采取先更新 j 取较大值的倒序
多重背包更新时,因为物品可以无限使用,所以无需担心物品被多次使用,而且需避免一件物品只能被用一次,故而采用正序

多重背包

题目

基本与01背包相同,但每件物品可以使用 s 次
<例题>

方法一

基本思路

从最基本的方面看,多重背包可以被看作有 s[ i ] 个 i 物品的01背包
由此得状态转移方程式:

f [ j ] = max( f [ j ],f [ j - c [ i ] *k ] + v [ i ] *k) || ( k <= s [ i ] )

只需在01背包代码基础上加一层循环即可

代码实现
#include<iostream>
using namespace std;
int c[1005],v[1005],s[1005];
int f[1005];
int main()
{
	int n,m;
	int i,j,k;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i]>>s[i];
	for(i=1;i<=n;i++)
	{
		for(j=m;j>=1;j--)
		{
			for(k=1;k<=s[i];k++)
			{
				if(j>=c[i]*k) f[j]=max(f[j],f[j-c[i]*k]+v[i]*k);
			}
		}
	}
	cout<<f[m];
	return 0;
} 

方法二(二进制优化时间复杂度)

基本思路

一般情况下上一种方法就够用了,这种方法不强求掌握
因为上一种方法直接采用01背包的方法,我们可以通过将 s 转为二进制再分别乘上 c 与 v 的方法将 s 个数组合成若干个可以组成不大于 s 的任意 2x
具体组合方法:

for(i=1;i<=n;i++)
{
	for(j=1;j<=s[i];j*=2)
	{
		cnt++;
		c1[cnt]=c[i]*j;
		v1[cnt]=v[i]*j;
		s[i]-=j;
	}
	if(s[i]>0) //赋值为体积为c[i]*s[i],价值为v[i]*s[i]的物品
	{
		cnt++;
		c1[cnt]=c[i]*s[i];
		v1[cnt]=v[i]*s[i];
	}
}

代码实现
#include<iostream>
using namespace std;
int n,m;
int c[12000],v[12000],s[12000];
int c1[12000],v1[12000];
int f[12000];
int main()
{
	int i,j,k,cnt=0;
	cin>>n>>m;
	for(i=1;i<=n;i++) cin>>c[i]>>v[i]>>s[i];
	for(i=1;i<=n;i++)
	{
		for(j=1;j<=s[i];j*=2)
		{
			cnt++;
			c1[cnt]=c[i]*j;
			v1[cnt]=v[i]*j;
			s[i]-=j;
		}
		if(s[i]>0) 
		{
			cnt++;
			c1[cnt]=c[i]*s[i];
			v1[cnt]=v[i]*s[i];
		}
	}
	for(i=1;i<=cnt;i++)
	{
		for(j=m;j>=c1[i];j--)
		{
			f[j]=max(f[j],f[j-c1[i]]+v1[i]);
		}
	}
	cout<<f[m];
	return 0;
} 

分组背包

题目

基本同01背包,但物品分为 k 组,每组中物品相互冲突
<例题>

具体做法

基本思路

对每一组数,可选择某一个物品,也可一个都不选
因而我们可以对每一组中的每个数进行更新,最后取到最优情况
状态转移方程式如下:

f [ j ] = max( f [ j ],f [ j - c [ i ][ k ] ] + v[ i ][ k ])

即对每组进行分别操作

代码实现
#include<iostream>
using namespace std;
int n,m;
int s[1005],c[1005][1005],v[1005][1005];
int f[1005];
int main()
{
	int i,j,k;
	cin>>n>>m;
	for(i=1;i<=n;i++)
	{
		cin>>s[i];
		for(j=1;j<=s[i];j++)
		{
			cin>>c[i][j]>>v[i][j];
		}
	}
	for(i=1;i<=n;i++)
	{
		for(j=m;j>=1;j--)
		{
			for(k=1;k<=s[i];k++)
			{
				if(c[i][k]<=j)
				f[j]=max(f[j],f[j-c[i][k]]+v[i][k]);
			}
		}
	}
	cout<<f[m];
	return 0;
} 

结束语

以上就是有关常见的背包问题的全部分析及讲解了,祝大家学业有成,工作顺利
在这里插入图片描述

  • 35
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值