背包

前言

背包线性动态规划的一种重要模型,它分为:

1、0/1背包

2、完全背包

3、多重背包

4、分组背包

我们将一起学习这种DP的思路开拓思维

0/1背包

题目原型

给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i],每种物品只有1个。

现挑选物品放入背包中,假定背包能承受的最大重量为 V,

问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

分析

对于这类问题,我们首先的思路可能是贪心。但是经过一番推导后,我们发现贪心的思路并不可行。那么对于线性动态规划有一定了解的同学们会发现,这题满足线性动态规划的原则(即无后效性和最优子结构性质),可以考虑使用动态规划算法解决。

那么我们想要使用DP,首先要把状态定义出来。刚才我们说过了,本题是一道线性DP。那么对于线性DP,我们一般将“阶段”定义进状态中。不难发现本题的阶段是对于第 \(i\) 件物品的处理,以及当前已经装进背包的重量。

于是,我们可以定义状态 \(f[i][j]\) 表示处理了前 \(i\) 件物品,当前背包中已经放了 \(j\) 的重量。那么,显然有转移方程:

f[i][j]=f[i-1][j]; //不选这件物品
f[i][j]=max(f[i][j],f[i][j-w[i]]+c[i]); //选这件物品

此时我们发现它的空间复杂度过高,为 \(O(nV)\) 。可是分析转移方程,可以发现,对于 \(f[i][j]\) ,它的转移只与 \(f[i-1][j]\) 有关,于是我们可以采用一种“滚动数组”的方法优化空间复杂度。优化后为:

f[i & 1][j]=f[(i-1) & 1][j];
f[i & 1][j]=max(f[i & 1][j],f[(i-1) & 1][j-w[i]]+v[i]);

分析到这里,整个代码已经出了大概样貌:

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int f[2][N],n,v;
int w[N],c[N];
int main()
{
    memset(f,0xcf,sizeof(f));
    f[0][0]=0;
    scanf("%d %d",&v,&n);
    for(int i=1;i<=n;i++)
        scanf("%d %d",&w[i],&c[i]);
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=v;j++)
            f[i & 1][j]=f[(i-1) & 1][j];
        for(int j=v;j>=w[i];j--)
            f[i & 1][j]=max(f[i & 1][j],f[i & 1][j-w[i]]+c[i]);
    }
    int ans=0;
    for(int i=0;i<=v;i++)
        ans=max(ans,f[n & 1][i]);
    printf("%d",ans);
    return 0;
}

这段代码已经可以过掉 P1048采药 了,但是还是不够完美。
怎么去进一步优化呢?分析代码,我们可以发现

每一步中,我们实际是把f[(i-1) & 1]复制到f[i & 1][j]中,
或是用f[(i-1) & 1][j]更新f[i & 1][j],
实际的转移与状态的第一维并无关,因此,我们不必定义第一维了。

如果理解不了建议自己把两种都打上,写个对拍看看是否有区别。

那么到了这一步,我们最通用的0/1背包模板出现了:

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int v,n,w[N],c[N],f[N];
int main()
{
	scanf("%d %d",&v,&n);
	memset(f,0xcf,sizeof(f));
  	f[0]=0;
	for(int i=1;i<=n;i++)
	    scanf("%d %d",&w[i],&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]]+c[i]);
	int ans=0;
  	for(int i=1;i<=v;i++)
            ans=max(ans,f[i]);
  	printf("%d",ans);
	return 0;
}

完全背包

题目原型

给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i],每种物品都有∞个。

现挑选物品放入背包中,假定背包能承受的最大重量为 V,

问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

思路分析

这类题目大家很容易发现,它就是每种物品可以使用无数次的0/1背包。我们仍旧定义状态\(f[i][j]\) 表示处理了前 \(i\) 件物品,当前背包中已经放了 \(j\) 的重量,转移方程仍然相同:

f[i][j]=f[i-1][j]; //不选这件物品
f[i][j]=max(f[i][j],f[i][j-w[i]]+c[i]); //选这件物品

但是,有读者可能会问了,这不就是 0/1背包 吗,它怎么区分出无穷多与单个的区别呢?
大家不妨思考一下 DP 的三要素:阶段、状态、决策。我们为了保证 01背包 的后续的决策不影响之前阶段的决策,采用了倒序循环。那么,既然完全背包每件物品有无数多个,我们就可以采用正序循环,让同一个物品可以对最优状态进行多次更新。

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int v,n,w[N],c[N],f[N];
int main()
{
	scanf("%d %d",&v,&n);
	memset(f,0xcf,sizeof(f));
  	f[0]=0;
	for(int i=1;i<=n;i++)
	    scanf("%d %d",&w[i],&c[i]);
	for(int i=1;i<=n;i++)
	    for(int j=w[i];j<=v;j++)
	        f[j]=max(f[j],f[j-w[i]]+c[i]);
	int ans=0;
  	for(int i=1;i<=v;i++)
        ans=max(ans,f[i]);
  	printf("%d",ans);
	return 0;
}

多重背包

题目原型

给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i],每种物品都有s[i]个。

现挑选物品放入背包中,假定背包能承受的最大重量为 V,

问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

思路分析

我们仍然先从定义的角度入手此题。不难发现,这题就是 \(s[i]\) 个 0/1背包 的合体版本,于是我们可以很容易的打出暴力版本,时间复杂度 \(O(NV\sum{\frac{V}{v_i}})\)

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

但是对于稍微难度大的题目,暴力版本的时间复杂度就不好使了。这个时候怎么办呢?我们不妨考虑一种优化方法:二进制拆分。

引理:对于任意正整数 \(N\) ,一定存在一个数列 \(K={k_0,k_1,k_2,...,k_n}\),使得 \(N=\sum_{i=0}^{n}2^{k_i}\)

emm,这是一个比较容易看出的定理,作者在此不作过多解释,免得浪费篇幅(

我们不妨对于每个物品的价值进行一次二进制拆分,这样通过二进制拆分的预处理,我们就得到了一个 \(O(NVlogV)\) 的优秀算法,这是代码:

for(int i=1;i<=n;i++)
	scanf("%d",&A[i]);
for(int i=1;i<=n;i++)
	scanf("%d",&B[i]);
for(int i=1;i<=n;i++)
{
	int vi=A[i],wi=1,mi=B[i];
	for(int j=1;j<mi;j=j*2)
	{
		c[++s]=vi*j;
		w[s]=wi*j;
		mi=mi-j;
	}
	if(mi)
	{
		c[++s]=vi*mi;
		w[s]=wi*mi;
	}
	//完全拆分
}
for(int i=1;i<=s;i++)
	for(int j=v;j>=c[i];j--) //用二进制拆分得到的重量进行一次0/1背包
		f[j]=f[j]+f[j-c[i]];

那么有没有更优的 \(O(nv)\) 算法呢?当然有,但是这篇博客面向广大初学者,学有余力的读者可以自行参阅《背包九讲》进行学习。

posted @ 2021-10-15 22:35  青D  阅读(61)  评论(0编辑  收藏  举报