浅析动态规划:二、背包问题

余生长醉 提交于 2019-11-27 13:41:18

一、01背包

有 n 件物品和一个容量为 v 的背包。第 i 件物品的费用为 w[i],价值是 c[i],在不超过背包最大容量的情况下是总价值最大。

【分析】

考虑到每种物品只有一件,所以这是一个 01背包问题,何为 01,就是对于一件物品,只有取1件或者取0件的选择。

用 f[i][j] 表示前 i 件物品消耗 j 体积获得的最大价值,显然,状态转移方程:f[i][j]=max{f[i-1][j],f[i-1][j-w[i]]+c[i]}

【代码段】

for(int i=1;i<=n;i++)
	for(int j=m;j>=1;j--)
	{
		if(j>=w[i])//如果剩余空间还能放第 i 件物品,再放和不放之间取舍
			f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+c[i]);
		else//否则只能不放
			f[i][j]=f[i-1][j];
	}

如果只用一维数组 f[i] 能不能达到一样的效果呢?把二维变成一维,用一个状态代表两个状态。

注意到上面的代码中,外层循环控制的是物品序号,所以每执行完一次就会得到前 i 件物品放入空间为 m 的最大价值。

如果转换成一维,用 f[v] 表示空间为 v 时获得的最大价值,同样是用外层循环控制物品序号,执行第 i 次前,

保存在数组中的是前 i-1 件物品放入空间为 m 的最大价值,不管它是怎么放的,我接下来都需要尝试能不能把第 i 件放进去,

把第 i 件放进去的理由只有:有足够空间放进去 and 相同空间消耗的情况下会比原来的价值高。

假设前 i-1 件物品已经把空间装满,这时是从满空间开始向前找还是全部拿出来从0空间开始放呢?显然是前者。

【代码段】

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

二、完全背包

有 n 种物品和一个容量为 v 的背包。第 i 种物品的费用为 w[i],价值是 c[i],在不超过背包最大容量的情况下是总价值最大。

【分析】

和 01背包不同的是,这里的物品不限量,所以它的策略就不是简单的取0件或者取1件。

如果用二维数组,状态转移方程为:f[i][j]=max{f[i-1][j-kw[i]+kc[i]}(0<=k*w[i]<=j)

【代码段】

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

同样,我们尝试把它转化成一维数组,

【代码段】

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

这里的 j 从小到大循环,为什么?因为在选了第 i 种物品后,还可以继续选,直到把空间用完。

所以在外层循环执行完第 i 次后,得到的 f[m] 就是前 i 种物品在限空间不限量的情况下选择得到的最大价值。

三、多重背包

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

【分析】

类似于完全背包和01背包的结合,所以有朴素算法

【代码段】

for(i=1;i<=n;i++)
	for(j=m;j>=0;j--)
		for(k=0;k<=a[i];k++)
		{
			if(j<k*w[i])break;
			f[j]=max(f[j-k*w[i]+k*c[i],f[j])
		}

看到了吗!超越了 n2 的算法!爆零的气息!

首先明确一点,十进制数都可以转换成二进制数,也就是一堆2的次方相加(疯狂暗示

对,没错,我们把第 i 种物品分成若干物品,每件都有一个系数,新物品的价值和费用就是原来的价值和费用乘这个系数,

这里必须是不重复的拆分,因为拆分的目的就是把物品分解从而降低复制度,所以不会像二进制一样每个拆分数都是2的次方。

这些系数分别是 1、2、4、……2(k-1)、n-(2k+1)。前 i-1 个数的和是 2k+1,所以第 i 个数为 n-(2k+1)。

这里 k 是不大于 log2n[i] 的整数,n-(2k+1)是按2的次方拆分完成后的剩余部分。

【代码段】

n1=0; 
for(i=1;i<=n;i++)//将每一件物品分成1、2、4、……2^(k-1)^、n-(2^k^+1)件 
{
	num=1;//从 1 开始
	cin>>x>>y>>sum;//最多买 s 件,空间为 x,价值为 y 
	while(sum>=num)//还可以拆分 
	{
		v[++n1]=x*num;//第 n1 件拆分物品的空间 
		w[n1]=y*num;//第 n1 件拆分物品的价值 
		sum-=num;
		num<<=1;//扩大一倍
	}
	v[++n1]=x*sum;
	w[n1]=y*sum;//余下的也要储存 
}
for(i=1;i<=n1;i++)//按 01背包求解
	for(j=m;j>=v[i];j--)
		f[j]=max(f[j],f[j-v[i]]+w[i]);

这里我们将一个算法的复杂度由 n[i] 优化到 log2n[i],足以说明拆分的独特之处,需要特别注意。

四、混合背包

背包体积为V ,给出N个物品,每个物品占用体积为V[i],价值为W[i],
物品可以取一件,取 a[i] 件,或者取无数件,取无数件用0表示。
在所装物品总体积不超过V的前提下所装物品的价值的和的最大值是多少?

把三种基础背包整合起来,其实很简单。

#include<iostream>
using namespace std;
int n,m,w[101],c[101],a[101],f[101];
int max(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	cin>>m>>n;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>c[i]>>a[i];
	for(int i=1;i<=n;i++)
	{
		if(!a[i])//完全背包 
		{
			for(int j=w[i];j<=m;j++)
				f[j]=max(f[j],f[j-w[i]]+c[i]);
		}
		else//01背包 
		{
			int x=a[i];
			for(int k=1;k<=x;k<<=1)//多重背包转换为01背包 
			{
				for(int j=m;j>=w[i]*k;j--)
					f[j]=max(f[j],f[j-w[i]*k]+c[i]*k);
				x-=k;
			}
			if(x)
				for(int j=m;j>=w[i]*x;j--)
                    f[j]=max(f[j],f[j-w[i]*x]+c[i]*x);
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

五、二维背包

有N件物品和一个容量为V,载重为U的背包。第i件物品的体积是a[i],重量是b[i],价值是w[i]。
求解将哪些物品装入背包可使价值总和最大,输出最大的总价值。

【分析】

费用加了一维,只需状态也加一维即可。设 f[i][v][u] 表示前 i 件物品付出两种代价分别为 v 和 u 时可获得的最大价值。

状态转移方程就是:f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]},把第一维的序号 i 去除,就变成了二维。

使用二维数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。

当物品有如多重背包问题时拆分物品。有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。

这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为 M。

换句话说,设 f[v][m] 表示付出费用 v、最多选 m 件时可得到的最大价值,

则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在 f[0…V][0…M]范围内寻找答案。

另外,如果要求“恰取M件物品”,则在f[0…V][M]范围内寻找答案。

#include<iostream>
#include<cstring>
using namespace std;
int a[101],b[101],w[101];
int f[101][101];
int v,u,n,i,j,k;
int max(int x,int y)
{
	return x>y?x:y;
}
int main()
{
	cin>>n>>v>>u;
	for(i=1;i<=n;i++)
		cin>>a[i]>>b[i]>>w[i];
	for(i=1;i<=n;i++)
		for(j=v;j>=0;j--)
			for(k=u;k>=0;k--)
			{
				int c1=j+a[i];
				int c2=k+b[i];
				c1=c1>v?v:c1;
				c2=c2>u?u:c2;
				f[c1][c2]=max(f[c1][c2],f[j][k]+w[i]);
			}
	cout<<f[v][u]<<endl;
	return 0;
}

当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一维以满足新的限制是一种比较通用的方法。

六、分组背包

有N件物品和一个容量为V的背包,第i件物品的重量为c[i],价值为w[i],这些物品被划分成了若干组,
每组中的物品互相冲突,最多选一件,问将哪些物品放入背包中可以使背包获得最大的价值。

【分析】

我们用 f[k][v] 表示前 k 种物品花费费用 v 所能取得的最大价值。

状态转移方程:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}

使用一维数组的伪代码:

for 所有的组k
	for v=V……0
		for 所有的 i 属于组 k
			f[v]=max{f[v],f[v-w[i]]+c[i]}

这里的循环顺序,第二层必须在第三层外面,这样才能保证每组最多有一个物品被添加到背包。

#include<iostream>
#include<algorithm>
#include<cstring> 
using namespace std;
int N,V,T;
int v[101],w[101];
int f[101];
int a[11][101];
int main()
{
    cin>>V>>N>>T; //容量,物品数,组数
    for(int i=1;i<=N;i++)
    {
        int p;
        cin>>v[i]>>w[i]>>p;
        a[p][++a[p][0]]=i; //存每一组的所有物品的编号 
        //a[p][0]表示第p组一共有几个物品 
    }
    for(int i=1;i<=T;i++)//从第一组开始
    	for(int j=V;j>=0;j--)//
    		for(int k=1;k<=a[i][0];k++)//枚举第一组的所有元素序号
    			if(j>=v[a[i][k]])
        			f[j]=max(f[j],f[j-v[a[i][k]]]+w[a[i][k]]);
    cout<<f[V]<<endl;
    return 0;
}

七、背包方案

给定 n 种面值,求组成面值为 m 的方案数。

设 f[m] 表示面值为 m 的方案数,根据动态规划的思想,有01背包写法和完全背包写法。

【01背包】

#include<cstdio>
using namespace std;
int a[101];
long long f[1001];//这里要开 long long
int main()
{
	int n,m,i,j,k;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
		scanf("%d",&a[i]);
	f[0]=1;
	for(i=1;i<=n;i++)
		for(j=m;j>=a[i];j--)
			for(k=1;k<=j/a[i];k++)
				f[j]+=f[j-k*a[i]];
	printf("%lld",f[m]);
	return 0;
}

【完全背包】

#include<cstdio>
using namespace std;
int a[101];
long long f[1001];
int main()
{
	int n,m,i,j,k;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
		scanf("%d",&a[i]);
	f[0]=1;
	for(i=1;i<=n;i++)
		for(j=a[i];j<=m;j++)
			f[j]+=f[j-a[i]];
	printf("%lld",f[m]);
	return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!