背包问题

这里写了作者学过的一些背包问题的解法,希望能为新入门DP的OIer提供便利。

\(\LaTeX\) 懒得修了,凑合着看吧QwQ~

2019/11/25:更新了混合背包。
2020/09/24:更新了多重背包的二进制优化和加二进制优化后的混合背包~

01背包

01背包解决的就是有一堆东西,有体积量和价值,要放到一个容量为m的背包里,使得价值之和最大。
重点: 每个物品只有1个。
之所以叫01背包,因为没个物品就是取或不取,取是1,不取是0。

思路:\(dp_{i,j}\)表示前i个物品放到容量为j的背包中的最大价值,则
\(dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-w_i}+c_i)\)
\(dp_{i-1,j}\)表示不取这个东西,那么容量还是\(j\)\(dp_{i-1,j-w_i}+c_i\)表示取,那么之前的容量就是\(j-w_i\)

优化: 我们发现,我们只需要\(dp_{i-1}\),而不需要更前面的数据,所以可以换成两个数组的滚动数组,然后,我们发现,只需要i之前的数保留即可,那么可以从后往前赋值,这样只要一个数组就能完成。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
//n是物品数,m是背包容量 
int w[505],c[505];
//wi表示第i个物品的重量,ci表示价值 
int dp[6005];
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i]>>c[i];
	for(int i=1;i<=n;i++)//枚举每个物品 
	 for(int j=m;j>=w[i];j--)//枚举背包容量 
	 	dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
	 	//状态转移方程 
	cout<<dp[m];
	return 0;
}

完全背包:

完全背包,就是一个物品能取无限次,求最大价值。

我们写01背包时之所以要从后往前,是要避免重复取一个东西,而完全背包就是一个物品能取无限次,所以只要把内层循环改成\(w_i\)~\(m\)即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
//n是物品数,m是背包容量 
int w[505],c[505];
//wi表示第i个物品的重量,ci表示价值 
int dp[6005];
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i]>>c[i];
	for(int i=1;i<=n;i++)//枚举每个物品 
	 for(int j=w[i];j<=m;j++)//枚举背包容量 
	 	dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
	 	//状态转移方程 
	cout<<dp[m];
	return 0;
}

多重背包

多重背包,第i个物品能取\(0\)~\(s_i\)个,求最大价值。

我们可以把多重背包的一个物品取多次看成多个一样的物品,例如,1号物品有2个,我们可以看成1和1。然后做01背包即可。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
//n是物品数,m是背包容量 
int w[505],c[505],s[505];
//wi表示第i个物品的重量,ci表示价值,si表示数量 
int dp[6005];
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>c[i]>>s[i];
	for(int k=1;k<=n;k++)//枚举物品种类 
	 for(int i=1;i<=s[k];i++)
	 //枚举这个种类的物品的个数 
	  for(int j=m;j>=w[k];j--)//枚举背包容量
	  	dp[j]=max(dp[j],dp[j-w[k]]+c[k]);
	 	//状态转移方程 
	cout<<dp[m];
	return 0;
}

多重背包优化:二进制优化

在一些情况中,我们如果把多重背包当01背包来处理,数量太多了,这时我们就需要二进制优化。
二进制优化就是把多个一样的物品分为1个一组,2个一组,4个一组……n个一组,再把剩下来的搞成一组。
我们都知道我们可以用2的\(1\) ~ \(n\)次幂表示\(1\) ~ \(2^{n+1}-1\)的数,所以这样做是可行的。
例题
代码:

#include<cmath>
#include<cstdio>
#include<algorithm>
#define rg register
using namespace std;
int T,n,m,mx,cnt,tmp;
int a[2005],v[2005];
int t[500005];
bool dp[500005];
inline int read()
{
    int x=0;int f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    return x*f;
}
int main()
{
	n=read(),m=read();
	for(rg int i=1;i<=n;i++)
	{
		int aa,vv;
		aa=read(),vv=read();
		for(rg int j=1;vv-j>=0;j*=2)
		{
			a[++cnt]=j*aa;
			vv-=j;
		}
		if(vv>0) a[++cnt]=vv*aa;
	}
	for(rg int i=1;i<=m;i++)
	{
		t[i]=read();
		if(t[i]>mx) mx=t[i];
	}
	dp[0]=true;
	for(rg int i=1;i<=cnt;i++)
	{
		while(dp[tmp]&&tmp<=mx) tmp++;
		if(tmp>=mx) break;
		int nd=max(tmp,a[i]);
		for(rg int j=mx;j>=nd;j--)
			if(dp[j-a[i]]) dp[j]=true;
	}
	for(rg int i=1;i<=m;i++)
		if(dp[t[i]]) printf("Yes\n");
		else printf("No\n");
	return 0;
}

分组背包

分组背包,就是把东西分成t组,每组最多取1个,最少不取,求最大价值。

我们可以把每组看成一样物品,只不过它的体积和价值是会变的,我们只要像做01背包那样,最后再循环判断每组里的物品就行了。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,t;
//n是物品数,m是背包容量,t是组数
int a[15][505];
//aij表示第i组的第j个物品的编号 
int w[505],c[505];
//wi表示第i个物品的重量,ci表示价值 
int dp[6005];
int main()
{
	cin>>m>>n>>t;
	for(int i=1;i<=n;i++)
	{
		int p;
		cin>>w[i]>>c[i]>>p;
		a[p][++a[p][0]]=i;//存储 
	}
	for(int i=1;i<=t;i++)//枚举每组物品 
	 for(int j=m;j>=0;j--)//枚举背包容量 
	  for(int k=1;k<=a[i][0];k++)
	  //枚举每组中的每个物品 
	   if(j>=w[a[i][k]])//判断是否可以放下这个东西 
	 	dp[j]=max(dp[j],dp[j-w[a[i][k]]]+c[a[i][k]]);
	 	//状态转移方程 
	cout<<dp[m];
	return 0;
}

当然混合背包中的多重背包也能用二进制优化啦~
例题
代码:

#include<cstdio>
using namespace std;
int n,m,t;
//n是物品数,m是背包容量 
int w[100005],c[100005];
//wi表示第i个物品的重量,ci表示价值
bool p[100005];
//pi表示状态 
int dp[1005];
int tsh,tsm,teh,tem;
int max(int x,int y){return x>y?x:y;}
int main()
{
	scanf("%d:%d %d:%d %d",&tsh,&tsm,&teh,&tem,&n);
	tsm+=tsh*60,tem+=teh*60;
	m=tem-tsm;
	for(int i=1;i<=n;i++)
	{
		int ww,cc,pp;
		scanf("%d%d%d",&ww,&cc,&pp);
		if(pp)
		{
			for(int j=1;pp-j>=0;j*=2)
			{
				t++;
				w[t]=j*ww;
				c[t]=j*cc;
				p[t]=true;
				pp-=j;
			}
			if(pp)
			{
				t++;
				w[t]=pp*ww;
				c[t]=pp*cc;
				p[t]=true;
			}
		}
		else
		{
			t++;
			w[t]=ww;
			c[t]=cc;
			p[t]=0;
		}
	}
	for(int i=1;i<=t;i++)//枚举每个物品 
		if(p[i])//如果是01 
			for(int j=m;j>=w[i];j--)//枚举背包容量 
			dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
			//状态转移方程 
		else
			for(int j=w[i];j<=m;j++)//枚举背包容量
				dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
				//状态转移方程 
	printf("%d\n",dp[m]);
	return 0;
}

混合背包(01背包+完全背包+多重背包)

混合背包,就是将多种背包混合在一起,先看题:混合背包

那么这种情况,我们可以分类讨论。回望之前的01背包和完全背包的代码,我们会发现,只有第二重循环的顺序不同 (废话,就是只改了哪里) 那么我们就可以在第二重循环前判断即可。什么?你不知道哪个是完全背包哪个是01背包?搞个数组标记不就行了嘛。
然后来看多重背包,那这个更好解决了!只要在输入时预处理,关键部分根本没变。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,t;
//n是物品数,m是背包容量 
int w[6005],c[6005],p[6005];
//wi表示第i个物品的重量,ci表示价值
//pi表示状态 
int dp[1005];
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int ww,cc,pp;
		cin>>ww>>cc>>pp;
		if(pp)
		 for(int j=1;j<=pp;j++)
		 {
		 	t++;
		 	w[t]=ww;
		 	c[t]=cc;
		 	p[t]=1;
		 }
		else
		{
			t++;
			w[t]=ww;
			c[t]=cc;
			p[t]=0;
		}
	}
	for(int i=1;i<=t;i++)//枚举每个物品 
	 if(p[i])//如果是01 
	  for(int j=m;j>=w[i];j--)//枚举背包容量 
	 	dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
	 	//状态转移方程 
	 else
	  for(int j=w[i];j<=m;j++)//枚举背包容量
	     dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
	 	//状态转移方程 
	cout<<dp[m];
	return 0;
}

持续更新中……只要这个蒟蒻学了新的背包类问题,就会更新。

posted @ 2020-08-16 19:47  Mine_King  阅读(163)  评论(0编辑  收藏  举报