背包问题详解

说起背包,可能大部分人都能立马反应到动规,而且背包一般都是九讲,但在我看来,实际上的背包问题只有01背包,分组背包和背包方案三种,接下来,我就针对这三种背包问题做一个详解。

一、01背包(完全背包,多重背包,混合背包,二维费用背包)
问题描述:有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

算法分析:
为什么会想到dp而不是贪心或者搜索之类的呢?
1、dp的证明:
很明显贪心是不行的,贪心在背包里只能满足部分最优而无法满足全局。我们假设x1,x2,x3······xn(值都为0/1)来表示第n件物品取或者是不取。那么这个问题就可以抽象成一个这样的数学模型:
answer=max(c1x1+c2x2+c3x3+······+cnxn)且
w1x1+w2x2+w3x3+···+wnxn ≤ V
假设x1,x2,x3···xn是这个问题的最优解,那么就有x2,x3,x4···xn是这个问题的子问题的最优解,假设(Y2,Y3,…,Yn)是上述问题的子问题最优解,则理应有(c2Y2+c3Y3+…+cnYn)+c1X1 > (c2X2+c3X3+…+cnXn)+c1X1;
  而(c2X2+c3X3+…+cnXn)+c1X1=(c1X1+c2X2+…+cnXn),则有(c2Y2+c3Y3+…+cnYn)+c1X1 > (c1X1+c2X2+…+cnXn);
  所以这个问题满足最优解,而用贪心得到的答案只是部分最优,而搜索会超时。
2、dp过程:
对面每一个物品来说都有两个状态,进包里和没进包里(0或1)。
把 f(i,j) 作为当前状态,i表示物品序号,j表示背包的剩余空间。
(1) j< ci
这是肯定放不进去的,所以这时候当前状态与上一状态是相同的,即 f(i,j)=f(i-1,j) 。
(2)j>ci
还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即f(i,j)=max ( f(i,j-ci)+wi,f(i-1,j) ) 。
都推到这里了,就不用我多说了吧。上代码。

for(int i=1;i<=n;i++)
{
	for(int j=0;j<=v;j++)
	{
		if(j<ci)	f[i][j]=f[i-1][j];
		else
		{
			f[i][j]=max(f[i-1][j],f[i][j-w[i]]+c[i];
		}
	}
}

二维数组虽然有些消耗空间(比起一维数组,背包有一维的办法),但是在有的问题中,比如要求找出物品的选择方法时,是一维无法解决的。
现在给出一组样例,让你输出物品的选择方法(选就输出1,不选就输出0)和最大价值
eg:number=4,capacity=8
c(体积):2 3 4 5
w(价值):3 4 5 6
输出为:
0 1 0 1
10

如果用的是二维数组,则只需要在上一段代码的最末尾加一段就OK了。

int i=n;
bool item[n+1];
memset(item,false,sizeof(item));
while(i>0)
{
	if(f[i][j]==f[i-1][j]	//这个物品未被取过
	{
		i--;
		continue;//继续判断下一个物品
	}
	else if(j>=w[i])
	{
		i--;
		j=j-w[i];
		item[i]=1;//标记一下,表示取过了。
	}
}
//最后再判断item就好了。

如果每一个背包都要打一手二维数组,想必数据一旦大了过后,各位的空间也开不下吧。所以我们可以想一下,这个二维数组里,哪一个状态是可以去掉的。很明显,i是可以去掉的。开二维数组,这个二维数组中的数是每一个i物品在j容量下的状态(可以自己把这个二维数组输出出来验证下)。所以现在我们调整一下数组,只开一个一维数组f[j]表示j容量下的背包利润最大值,所以现在这个问题的状态转移方程就变成了这样:f[j]=max(f[j],f[j-c[i]]+w[i])。
代码如下:

#include<iostream>
using namespace std;
int c[100],w[100],f[100];
int m,n;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>c[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=c[i];j--) //这个地方一定要注意是逆序
		{
			f[j]=max(f[j],f[j-c[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}

`为什么那个地方要逆序呢,从代码中我们可以看出,每一个f[j]的值都是取决于之前的那个状态。我用之前的那个数据做一个说明吧。
eg:number=4,capacity=8
c(体积):2 3 4 5
w(价值):3 4 5 6
如果逆序,第一件物品(第一次循环)进去过后,数组中的状态是这样的:
f[1~8] : 0 3 3 3 3 3 3 3
如果顺序,数组中的状态是这样的:
f : 0 3 3 6 6 9 9 12
还记得f[j]表示什么吗?表示在背包容量还剩j的时候,所能取得的最大价值,也就是说f中值的每一种状态都是取决于前一个状态,所以在逆序的情况下,状态f[j]的值是与f[j-1]的比较中产生的,此时f[j-1]为0,所以每个物品只判断了一次,而顺序的话,每个物品相当于判断了无数次,所以不得行。
基于这个思路,完全背包就很好解决了。
完全背包:完全背包问题是指每种物品都有无限件,有N种物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
代码如下:

#include<iostream>
using namespace std;
int c[100],w[100],f[100];
int m,n;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>c[i]>>w[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=c[i];j--) //这个地方一定要注意是顺序
		{
			f[j]=max(f[j],f[j-c[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}

多重背包:有N种物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i],有x[i]个。求解将哪些物品装入背包,装几件可使价值总和最大。
在读入数据过后,就可以对物品进行一些处理了。
在处理之前,先讲一个小小的结论:任何一个正整数都可以拆分成n个二的幂的和。
以100为例:100=1+2+4+8+16+32+37
所以100以内的所有数都可以用以上几个数凑出来(不信可以自己试)
那么现在一个可以选100次的一种物品就变成了7种物品。
for instance:
一个c为2,w为3,可以选100次的物品就变成了
c1=2 , w1=3
c2=4 , w2=6



c7=74,w7=111
的这样的7个物品。
然后就可以01背包把这个题干起了!我就不附上代码了(楼主好水啊!!!)
咳咳!
最后就是混合背包。顾名思义,就是前三个背包的混合。所以我们可以先做一点预处理,如果这个多重背包的物品数乘以它所占的空间数大于了背包容积,那么就可以把它看成一个完全背包,其他的就变成01背包。然后根据01背包和完全背包的套路分别处理就OK了。
代码如下:

#include<iostream>
using namespace std;
int f[101]={0};
int c[101],w[101],num[101];
int n,m;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>c[i]>>w[i]>>num[i];//如果是完全背包,num就是0
		if(c[i]*num[i]>m)
		{
			num[i]=0;
		}
	}
	int k=n;
	for(int i=1;i<=n;i++) //处理多重背包 
	{
		if(num[i]!=0)
		{
			int a=1;
			while(a<num[i])
			{
				k++;
				c[k]=c[i]*a;
				w[k]=w[i]*a;
				num[k]=a;
				num[i]-=a;
				a=a*2;
			}
		}
	}
	for(int i=1;i<=k;i++)
	{
		if(num[i]==0) //完全背包 
		{
			for(int j=c[i];j<=m;j++)
			{
				f[j]=max(f[j],f[j-c[i]]+w[i]);
			}
		}
		else
		{
			for(int j=m;j>=c[i];j--) //01背包 
			{
				f[j]=max(f[j],f[j-c[i]]+w[i]);
			}
		}
	}
	cout<<f[m];
	return 0;
}

哦,对了,还有个二维费用背包,其实二维背包就比一维背包多了一个维度而已,先来个例题吧!
题目描述
你现在有N个人选,每个人都有这样一些数据:A(能得到多少资料)、B(伪装能力有多差)、C(要多少工资)。已知敌人的探查间谍能力为M(即去的所有人B的和要小于等于M)和手头有X元钱,请问能拿到多少资料?

算法分析
很显然,它比01背包多了一个限制条件,抽象成数学模型,就是这样的:
answer = max ( A1x1+A2x2+…+Anxn) (xi=0或1) 且
B1
X1+B2X2+…+BnXn<M 且
C1X1+C2C2+…+Cn*Xn<X
所以针对以上条件,来找这个问题的状态转移方程:
我们可以用 f[i][j][k] 来表示在伪装能力为j,手头钱数还有k 的状态下,能取得的最多的资料,i为特工的编号。所以就是判断需要哪些人可以选使这个资料数最大。
推导过程就不细讲了,和前面01背包差不多,直接写方程吧,写出来就算你开始不明白也应该懂了。
f[i][j][k] = max(f[i-1][j][k],f[i][j-bi][k-ci])
也可以用滚动数组把它变成这样:
f[j][k] = max(f[j][k],f[j-bi][k-ci])
代码如下;

#include<bits/stdc++.h>
using namespace std;
int m,n,x;
int a[110],b[110],c[110];
int f[1010][1010]={0};
int main()
{
	cin>>n>>m>>x;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i]>>b[i]>>c[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=b[i];j--)
		{
			for(int k=x;k>=c[i];k--)
			{
				f[j][k]=max(f[j][k],f[j-b[i]][k-c[i]]+a[i]);
			}
		}
	}
	cout<<f[m][x];
	return 0;
}

这是01类型的二维背包,完全背包类型的和多重背包类型的也是同一套路。
再讲一下泛化物品的问题,其实所谓的泛化物品,就是把这个物品看成是完全背包,仔细想一下,这个物品占用的体积为x时,它的价值为f(x)(函数,如ax^2+bx+c这种),就可以把这个东西看成个完全背包。

以上几个背包问题的洛谷模板题:P1910,P1048,P1616,在背包问题的普通应用上还有问题的可以去刷一下,稍微复杂一点的就有:P1833,P1782,有兴趣的可以去尝试一下。

二、分组背包(有依赖的背包问题)

【题目描述】
一个旅行者有一个最多能装V公斤的背包,现在有n件物品,它们的价值分别是W1,W2,…,WnW1,W2,…,Wn,它们的重量分别为C1,C2,…,CnC1,C2,…,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

算法分析:
可以看出,这个题目比起普通的01背包多了一个状态,那就是物品的组别,每组最多选择一件,那么就在原01背包的基础上,我们可以写出一个新的DP方程,f[k][i][j]=max(f[k][i][j],f[k][i-1][j-ci]+wi).k表示的是组别数,我们只需要在一组一组地去查看就好了。所以这道题三重循环是肯定无法避免的。
还是贴个代码。

#include<cstdio>
#include<iostream>
#define zero 0; //快乐归零
using namespace std;
int v,n,t;
int w[10010],c[10010];
int a[1010][10100];
int f[20100];
int main()
{
	cin>>v>>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 k=1;k<=t;k++) //所有的组 
	{
		for(int j=v;j>=0;j--)
		{
			for(int i=1;i<=a[k][0];i++) //组中的所有数 
			{
				if(j>=w[a[k][i]])
				{
					f[j]=max(f[j],f[j-w[a[k][i]]]+c[a[k][i]]);
				}
			}
		}
	}
	cout<<f[v];
	return zero;//相当快乐
}

然后就是有依赖的背包问题。
题目参见添加链接描述
本题和上面的分组背包就有一点不一样,要先取主件,所以我们在上面分组背包的基础上多一个判断就好了,也就是要先取出上面里的第一个。
也附个代码好了

#include<iostream>
#include<stdio.h>
using namespace std;
int n,m;
int v,p,q,vm[61],cm[61];
int r=0;
int aw[61][3]={0},ac[61][3]={0};
int f[32010]={0};
int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>v>>p>>q;
		if(q==0)
		{
			vm[i]=v;
			cm[i]=v*p;
		}
		else
		{
			aw[q][0]++;
			aw[q][aw[q][0]]=v;
			ac[q][aw[q][0]]=v*p;
		}
	}
	for(int i=1;i<=m;i++)
	{
		for(int j=n;j>=vm[i]&&vm[i]!=0;j--)
		{
			f[j]=max(f[j],f[j-vm[i]]+cm[i]);
			if(j>=vm[i]+aw[i][1]&&aw[i][1]!=0)
			{
				f[j]=max(f[j],f[j-vm[i]-aw[i][1]]+cm[i]+ac[i][1]);
			}
			if(j>=vm[i]+aw[i][2]&&aw[i][2]!=0)
			{
				f[j]=max(f[j],f[j-vm[i]-aw[i][2]]+cm[i]+ac[i][2]);
			}
			if(j>=vm[i]+aw[i][1]+aw[i][2]&&aw[i][1]!=0&&aw[i][2]!=0)
			{
				f[j]=max(f[j],f[j-vm[i]-aw[i][1]-aw[i][2]]+cm[i]+ac[i][1]+ac[i][2]);
			}
		}
	}
	cout<<f[0];
	return 0;
}

背包方案的话其实和背包问题就没有太多了联系了,只是套了背包外壳的dp。
这里就不多说了。
上面那道金明的预算方案如果附件还可以作为其他附件的主件,这道题是一个树形dp。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值