背包DP详解

目录


1.01背包
2.完全背包
3.多重背包
4.混合背包
5.分组背包
6.二维费用背包

1.01背包

题目简介:

有n个物品和一个容量为v的背包,每个物品的价值为c[i],体积为w[i],要求选择一些物品放入背包中,使物品总体积不超过m的前提下,物品的总价值最大,求最大总价值。

基本思路:

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

f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+c[i]);
/*
f[i-1][v]为不取第i件物品,让前i-1件物品来装v这么大的空间
f[i-1][v-w[i]]+c[i]为取第i件物品,让前i-1件物品来装v-w[i]这
么大的空间(因为要给第i件物品腾空间,所以是v-w[i]),然后加上
第i件物品的价值。
这里要注意,j要大于等于w[i]
*/

核心代码如下:

for(int i=1;i<=n;i++) {
	for(int j=0;j<=m;j++) {
		f[i][j]=f[i-1][j];
		if(f[i-1][j-w[i]]+c[i]>f[i][j])
			f[i][j]=f[i-1][j-w[i]]+c[i];
	}
}

优化:

细细一品,其实我们可以用滚动数组来优化,因为我们可以发现第 i 个阶段的决策只与第 i - 1阶段有关,所以我们第一维只用开2;
核心代码:

int f[2][1005];
//第二维的大小就是v的范围,我这里是随便编得一个
for(int i=1;i<=n;i++) {
	for(int j=0;j<=m;j++) {
		f[i&1][j]=f[(i-1)&1][j];
		if(f[(i-1)&1][j-w[i]]+c[i]>f[i&1][j])
			f[i&1][j]=f[(i-1)&1][j-w[i]]+c[i];
	}
}

再细细一品,在每个阶段的开始时,实际上执行了一次从f[ i-1 ][]到f[ i ][]的拷贝操作,于是我们就可以将二维改为一维。f[ j ]表示背包中放入总体积为 j 的物品的总价值。
核心代码:

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

注意,这里我们是用的倒序循环。循环到j时:
f数组中f[j~m]处于第i个阶段,f[0 ~j-1]处于第i-1个阶段。
当 j 逐渐减小时,满足我们的要求:总是用第i-1个阶段的状态向第i个阶段转移。
但如果我们用正序,则就会出现一个物品用多次的情况,不符合01背包。

2.完全背包

题目简介:

有n个物品和一个容量为v的背包,每个物品的价值为c[i],体积为w[i],每个物品有无数件,要求选择一些物品放入背包中,使物品总体积不超过m的前提下,物品的总价值最大,求最大总价值。

贪心思路:

一看题目,脑子第一反应的就是贪心,先排序,按c[i]/w[i]这个比值从大到小排序,然后来装。先让比值最大的来装,直到装不下为止,然后考虑第二件…
代码:

#include 
using namespace std;
int n,m,ans;
struct node{
	int w,c;
	double bi;
}a[205];

bool cmp(node x,node y) {
	return x.bi>y.bi;
}

int main() {
	scanf("%d %d",&m,&n);
	for(int i=1;i<=n;i++) {
		scanf("%d %d",&a[i].w,&a[i].c);
		a[i].bi=double(a[i].c)/double(a[i].w);
	}
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++) {
		while(a[i].w<=m) {
			m-=a[i].w;
			ans+=a[i].c;
		}
	}
	printf("%d",ans);
	return 0;
}

当然,有比较大的几率会TLE。

DP基本思路:

其实从01背包就发现了,当一维做法第二重循环为正序时,就有可能一件物品取多次了,完全符合完全背包的条件
核心代码:

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

一定要切记切记再切记,j的范围是m,不是n!(本蒟蒻在此处死惨了)

3.多重背包

题目简介:

有n个物品和一个容量为v的背包,每个物品的价值为c[i],体积为w[i],每个物品有r[i]件,要求选择一些物品放入背包中,使物品总体积不超过m的前提下,物品的总价值最大,求最大总价值。

基本思路:

1.直接拆分法

求解多重背包最直接的方法就是把第i种物品看作独立的r[i]件物品,转化为01背包问题来解答。

#include 
using namespace std;
int n,m,w[105],c[105],r[105],f[105],ans;
int main() {
	scanf("%d %d",&m,&n);
	for(int i=1;i<=n;i++)
		scanf("%d %d %d",&w[i],&c[i],&r[i]);
	for(int i=1;i<=n;i++) {
		for(int k=1;k<=r[i];k++) {
			for(int j=m;j>=w[i];j--) {
				f[j]=max(f[j],f[j-w[i]]+c[i]);
				ans=max(ans,f[j]);
			}
		}
	}
	printf("%d",ans);
	return 0;
} 

这样的时间复杂度稍稍有一点高,我们可以用二进制拆分法来优化。

2.二进制拆分法

众所周知(你们知不知道我不知道,但我之前是不知道的):
背包DP详解_第1张图片
代码:

#include 
using namespace std;
int n,m,v,w[555],c[555],f[555];
int main() {
	scanf("%d %d",&v,&m);
	for(int i=1;i<=m;i++) {
		int kongjian,jiage,shuliang;
		scanf("%d %d %d",&kongjian,&jiage,&shuliang);
		for(int k=1;k<=shuliang;k<<=1) {
			n++;
			w[n]=kongjian*k;
			c[n]=jiage*k;
			shuliang-=k;
		}
		if(shuliang!=0) {
			n++;
			w[n]=kongjian*shuliang;
			c[n]=jiage*shuliang;
		}
	}
	
	for(int i=1;i<=n;i++) {
		for(int j=v;j>=w[i];j--) {
			if(f[j-w[i]]+c[i]>f[j])
				f[j]=f[j-w[i]]+c[i];
		}
	}
	printf("%d",f[v]);
	return 0;
} 

4.混合背包

题目简介:

有n类物品和一个容量为m的背包,每类物品的体积为wi,价格为ci,数量为ni(ni等于-1代表该物品只能用1次,ni等于0代表该物品可以使用无限件),问如何装取物品使得装入背包中的物品总价格最高?

基本思路:

我们可以发现,01背包是倒序循环,完全背包是正序循环,多重背包也是倒叙循环。这仨兄弟之间就差了顺序罢了。只用在第二重循环时判断一下需要正序还是倒序就完全可以了。
代码:

#include 
using namespace std;
int m,n,v,c[10005],w[10005],f[1005];
bool s[10005];
int main() {
	scanf("%d %d",&m,&v);
	for(int i=1;i<=m;i++) {
		int vi,pi,ni;
		scanf("%d %d %d",&vi,&pi,&ni);
		if(ni==-1) {
			n++;
			s[n]=1;
			c[n]=pi;
			w[n]=vi;
		}
		else if(ni==0) {
			n++;
			s[n]=0;
			c[n]=pi;
			w[n]=vi;
		}
		else {
			for(int k=1;k<=ni;k<<=1) {
				n++;
				c[n]=k*pi;
				w[n]=k*vi;
				ni-=k;
				s[n]=1;
			}
			if(ni) {
				n++;
				s[n]=1;
				c[n]=ni*pi;
				w[n]=ni*vi;
			}
		}
	}
	
	for(int i=1;i<=n;i++) {
		if(s[i]) {
			for(int j=v;j>=w[i];j--)
			    f[j]=max(f[j],f[j-w[i]]+c[i]);
		}
		else {
			for(int j=w[i];j<=v;j++)
			    f[j]=max(f[j],f[j-w[i]]+c[i]);
		}
	}
	printf("%d",f[v]);
	return 0;
}

5.分组背包

题目简介:

给定N组物品,其中第i组物品有num[i]个物品。第i组的第j个物品的体积为vij,价值为pij。有一容积为V的背包,要求选择若干个物品放入背包,使得每组至多选择一个物品并且物品总体积不超过V的前提下,物品的价值总和最大。

基本思路:

这也可以看作一个01背包,只是需要多一个循环来看一看这一种物品中哪一个物品更好罢了。
代码:

#include 
using namespace std;
int n,v,w[105][105],c[105][105],f[105],sum[105];
int main() {
	scanf("%d %d",&n,&v);
	for(int i=1;i<=n;i++) {
		scanf("%d",&sum[i]);
		for(int j=1;j<=sum[i];j++)
		    scanf("%d %d",&w[i][j],&c[i][j]);
	}
	for(int i=1;i<=n;i++) {
		for(int j=v;j>=0;j--) {
			for(int k=1;k<=sum[i];k++) {
				if(j>=w[i][k])
				    f[j]=max(f[j],f[j-w[i][k]]+c[i][k]);
			}
		}
	}
	printf("%d",f[v]);
	return 0;
} 

6.二维费用背包

我们先直接举个例子:潜水员。

潜水员有一定数量的气缸,每个气缸都有重量和气体容量。气缸同
时带有2种气体:一种为氧气,一种为氮气。潜水员为了完成潜水工作需要一定数量的氧气和氮气,允许他带多个气缸完成工作。 他所需气缸的总重量最少是多少?如果不能恰好带这么多气体,允许多
带一些(先考虑氧气最小,再考虑氮气最小)。
输入第一行有3整数u, v, n(1<=u, v<=1000),表示氧,氮各自需要的量, n(1<=n<=100)表示气缸的个数。
此后的n行,每行包括ai,bi,ci(1<=ai<=100,1<=bi<=100,1<=ci<=800),分别表示第i个气缸里的氧和氮的容量及汽缸重量。

因为允许多带一些,所以我们在循环的时候就得让多循环一些。加100就可以了(你多带一个就可以了,再多了也没用)。
代码:

#include 
using namespace std;
int v,u,n,a[1005],b[1005],c[1005];
int f[1005][1005];
int main() {
	scanf("%d %d %d",&v,&u,&n);
	memset(f,0x3f3f3f3f,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=n;i++)
	    scanf("%d %d %d",&a[i],&b[i],&c[i]);
	for(int i=1;i<=n;i++) {
		for(int j=v+100;j>=a[i];j--) {
			for(int k=u+100;k>=b[i];k--) {
				f[j][k]=min(f[j][k],f[j-a[i]][k-b[i]]+c[i]);
			}
		}
	}
	for(int i=v;i<=v+100;i++) {
		for(int j=u;j<=u+100;j++) {
			if(f[i][j]!=0x3f3f3f3f) {
				printf("%d\n",f[i][j]);
				return 0;
			}
		}
	}
	printf("-1");
	return 0;
}

好好品一品,相信你能明白的,感觉就想一个复杂一点点的01背包。


你可能感兴趣的:(DP)