背包问题模板汇总

介绍

背包问题(Knapsack problem)是一种组合优化的NP完全问题。它是在1978年由Merkel和Hellman提出的。

问题的描述为:

有一个背包,最多放M kg的物体(物体大小不限);
有n个物体,每个物体的重量为Wi,每个物体完全放入背包后可获得收益Vi。问: 如何放置能获得最大的收益?

背包问题一般分为两种,若每个物体不可拆分,则称为0-1背包问题(和其引申问题),这种问题无法用贪心法求的最优解,应该用动态规划的思想。而若每个物体可以切分,则称为一般背包问题,可以使用贪心法求的最优解。

一般背包

对于一般背包问题,其思想就是贪心。
贪心策略:先将每一件物品权值与重量的比值按从大到小的顺序排序,然后依次放入背包,也就是性价比最高的先放入背包。

0-1背包

描述

有N件物品和一个容量为V的背包。第i件物品的费用是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。

这是最常见的背包问题,其特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即a[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值。

当不放的时候则是:a[i][j] = a[i-1][j];
放的时候则是:a[i][j] = a[i-1][j-w[i]]+v[i];

如果不放,就保持上一种状态,如果放,就在牺牲容积的情况下增加价值。由于我们求的是最大价值,则其状态转移方程便是:
a[i][j]=max{ a[i-1][j], a[i-1][j-w[i]]+v[i] }。

	for(int i=1;i<=n;i++)
		for(int j=V;j>=1;j--)
		{
			if(j>=w[i])
			a[i][j]=max(a[i-1][j],a[i-1][j-w[i]]+v[i]);
			else
			a[i][j]=a[i-1][j];
		}

一维数组优化

以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。
先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1…N,每次算出来二维数组f[i][0…V]的所有值。那么,如果只用一个数组f [0…V],能不能保证第i次循环结束后f[j]中表示的就是我们定义的状态f[i][j]呢?
f[i][j]是由f[i-1][j]和f [i-1][j-w[i]]两个子问题递推而来,能否保证在推导f[v]时(也即在第i次主循环中推f[j]时)能够得到f[j]和f[j -w[i]]的值呢?事实上,这要求在每次主循环中我们以v=V…0的顺序推f[j],这样才能保证推f[j]时f[j-w[i]]保存的是状态f[i-1][j-c[i]]的值。

代码如下:

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

完全背包

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][j]表示前i种物品恰放入一个容量为j的背包的最大权值。最终得出结论,完全背包和0-1背包的状态方程是基本一致的,但是为什么完全背包的循环是正序[w[i],V],而0-1背包的循环是倒序[V,w[i]],
这是因为完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][j-w[i]],正序则正好满足其条件。

for(int i=1;i<=n;i++)
		for(int j=1;j<=V;j++)
		{
			if(j>=w[i])
			a[i][j]=max(a[i-1][j],a[i-1][j-w[i]]+v[i]);
			else
			a[i][j]=a[i-1][j];
		}

一维优化同0-1背包:

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

多重背包

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

与完全背包一样,多重背包也是转化为0-1背包问题来做:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解,时间复杂度为O(V*Σn[i])。

for(int i=1;i<=n;i++)
	for(int j=V;j>=w[i];j--)
		for(int k=1;k<=n[i]&&j-k*w[i]>=0;k++)
			f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);

二进制优化

在多重背包的问题中,很多时候Σn[i]会非常大,很有可能超时。
因为1、2、4、8 、16 、 32……2 n 可以组成从1到2(n+1)-1中的任何数
用二进制的思想,我们将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,…,2(k-1),n[i]-2 k +1,且k是满足n[i]-2k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
这样就把原问题转化为了复杂度为O(V*Σlog n[i])的01背包问题。

for(int i=1;i<=n;i++)
{
	for(j=1;j<=a[i].num;j<<=1)//分解为2^x
	{
		v[tot]=j*a[i].v;
		w[tot]=j*a[i].w;
		tot++;
		a[i].num-=j;
	}
	if(a[i].num>0)//最后一项补齐
	{
		v[tot]=a[i].num*a[i].v;
		w[tot]=a[i].num*a[i].w;
		tot++;
	}
}

混合背包

如果将0-1背包、完全背包、多重背包混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?

设p[i]=0,1,n(n!=0,1)分别为0-1背包、完全背包、多重背包。
代码如下:

#include <cstdio>
#include <iostream>
#define maxn 10005
using namespace std;
int t[maxn],c[maxn],p[maxn];
int main()
{
	int f[maxn];
	int ts1,ts2,te1,te2;
	scanf("%d:%d %d:%d",&ts1,&ts2,&te1,&te2);
	int time=(te1-ts1)*60+te2-ts2,n;
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>t[i]>>c[i]>>p[i];
	for(int i=1;i<=n;i++)
	{
		if(p[i]==0)//完全背包 
		{
			for(int j=V;j>=w[i];j--)
				f[j]=max(f[j],f[j-w[i]]+v[i]);
		}
		else if(p[i]==1)//01背包 
		{
			for(int j=w[i];j<=V;j++)
				f[j]=max(f[j],f[j-w[i]]+v[i]);
		}	
		else//多重背包 
		{
			for(int j=V;j>=w[i];j--)
				for(int k=1;k<=p[i]&&j-k*w[i]>=0;k++)
					f[j]=max(f[j],f[j-w[i]*k]+v[i]*k);
		}
	}
	cout<<f[V];
	return 0;
}
  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值