背包问题
背包问题
1 01背包问题
1.1 问题:
有\(N\)件物品和容量为\(V\)的背包,放第\(i\)件物品的体积是\(C_i\),得到的价值是\(W_i\),问放入背包哪些物品能使价值总和最大。
1.2 思路:
首先,在类似的问题中,贪心思想是错误的,这点可以自己思考一下。
在这样一个问题中,我们思考经典的动态规划的思路,对于每一个物品,我们有两种策略:放,或不放。
我们定义\(F[i,v]\)为前\(i\)件物品恰好放入容量为\(v\)的背包可以得到的最大价值。
放:\(F[i,v]=F[i-1,v-C_i]+W_i\)
不放:\(F[i,v]=F[i-1,v]\)
for(int i=1;i<=n;i++)
{
for(int j=c[i];j<=v;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
}
}
1.3 一些优化:
以上想法的时间空间复杂度均为\(O(VN)\) ,很显然时间复杂度不能再往下优化了。
但空间复杂度还可以优化,因为我们的\(F(i,v)\)都是从\(F(i-1,x)\) 递推而来,也用不到\(F(i,x)\),考虑把二维压缩成一维 。
for(int i=1;i<=n;i++)
{
for(int j=V;j>=c[i];j--)
{
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
}
}
这样只需保证在更新\(dp[j]\) 时,要用到的\(dp[j]\)和\(dp[j-c[i]]\) 都还未在当轮被更新,保证我们用到的是\(dp[i-1][j]\)和\(dp[i-1][j-c[i]]\) ,那么具体的实现就是把内层循环倒着跑,这样在更新\(dp[j]\)时,确保比他小的\(dp[j-c[i]]\)还未被更新
模板题代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn];
int n,v;
void ZeroOnePack(int c,int w)
{
for(int i=v;i>=c;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=1;i<=v;i++) dp[i]=0;
for(int i=1;i<=n;i++) ZeroOnePack(c[i],w[i]);
printf("%d\n",dp[v]);
return 0;
}
内层循环的下限其实可以改为\(max(V-\sum_{j=i}^{N}w[j],c[i])\)
我们知道空间优化后的状态转移方程为\(dp[i]=max(dp[j],dp[j-c[i]]+w[i])\) (i为物品编号,j为当前体积v),那么对于\([i+1,n]\)的情况,这里的\(j-c[i]\) 最多取值也就取到\(\sum_{j=i}^{N}w[j]\),而\(\sum_{j=i}^{N}w[j]\) 到\(c[i]\) 也不会取到,也就没有计算的必要
既然这样,我们就不用更新到过左的位置,也就是只需保证当前物品以及后面的物品都能放下就可以了
这种优化在背包体积很大时很有优势
被优化代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn],sum[maxn];
int n,v;
void ZeroOnePack(int c,int w,int sum)
{
int del=max(c,v-sum);
for(int i=v;i>=del;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=n;i>=1;i--) sum[i]=sum[i+1]+w[i];
for(int i=1;i<=v;i++) dp[i]=0;
for(int i=1;i<=n;i++) ZeroOnePack(c[i],w[i],sum[i]);
printf("%d\n",dp[v]);
return 0;
}
1.4 一些细节
在模板题中,题目并没有对是否要装满背包做出要求,但在一些其他的问题中会要求“恰好装满背包”的最优解,而区别在于初始化。
如果是恰好装满背包,那除了\(dp[0]=0\)外,\(dp[i]=-inf\ (i\in[1,V])\) 因为\(dp[i]\)代表容量为i的背包被恰好装满时的价值,我们\(dp[0]\)可以理解为:容量为0的背包被“nothing”恰好装满时的价值为\(0\),但其他的i并没有类似的合法的解,属于一个未定义的状态。
同理,未被要求必须恰好装满时任何容量的背包都有一个合法的解,那就是装了“nothing”时的价值为\(0\)。
2 完全背包问题
2.1 题目:
有\(N\)种物品和容量为\(V\)的背包,每种物品可以无限取用,放第\(i\)种物品的体积是\(C_i\),得到的价值是\(W_i\),问放入背包哪些物品能使价值总和最大。
2.2 思路:
与01背包唯一不同的地方在于,他的每种物品有无限多个,而01背包每种物品只能取一次,而每种物品的策略也从(取或不取)两种变成了(取0件,取1件,取2件,...,取\(\left \lfloor V/C_i \right \rfloor\)件)
如果仍用01背包的想法,把每种物品取多少,想成是每种物品的每一件我取还是不取
我们定义\(F[i,v]\)为前\(i\)种物品恰好放入容量为\(v\)的背包可以得到的最大价值,得到状态转移方程
\(F[i,v]=max\left\{F[i-1,v-kC_i]+kW_i|0\leq kC_i\leq v\right\}\)
01背包的时间复杂度为\(O(NK)\) ,每种物品只有两个状态,完全背包每种物品有\(\left \lfloor V/C_i \right \rfloor+1\) 个状态,时间复杂度为\(O(NK\sum\frac{V}{C_i})\)
2.3 试着优化下
与01背包相比,这样的时间复杂度未免过于大了,我们考虑是否有方法把时间复杂度降下来
- 若两件物品\(i,j\) 满足\(C_i\leq C_j\) 且\(W_i\leq W_j\) ,则可以不用考虑\(j\)了
可以先将费用大于\(V\)的去掉,然后去找费用相同的物品,价值最高的那一个
虽然这种优化能大大减少物品的件数,但貌似并不能改善最坏情况下的时间复杂度
- 因为每个数都可以得到他的二进制表示,那么每一个问题的可行的答案都可以用满足\(C_i2^k\leq V\) 的非负整数\(k\) (费用为\(C_i2^k\),价值\(W_i2^k\))的物品来表示
这样就可以把每种物品拆成\(O(log\left \lfloor V/C_i \right \rfloor)\) 件物品
- 在01背包中,我们让内层循环倒着跑的原因是只想让\(F(i,x)\) 用到\(F(i-1,x)\) 而不是还未更新的\(F(i,x)\) ,以保证每件物品只选一次,但如果是完全背包,就没有这种顾虑了
for(int i=1;i<=n;i++)
{
for(int j=c[i];j<=v;j++)
{
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
}
}
我们发现,把这种写法回退到初始的二维,是这样的
\(F[i,v]=max\left\{F[i-1,v],F[i,v-C_i]+W_i\right\}\)
我们是否能给他一个合理的解释呢
确实,还是最初的那个取还是不取的问题,取?取\(F[i,x]\)一定会比\(F[i-1,x]\)要优吧 ( \(F[i-1,x]\leq F[i,x]\) ),不取?那自然还是\(F[i-1,v]\) 了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn];
int n,v;
void CompletePack(int c,int w)
{
for(int i=c;i<=v;i++)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&w[i]);
for(int i=1;i<=n;i++) CompletePack(c[i],w[i]);
printf("%d\n",dp[v]);
return 0;
}
3 多重背包问题
3.1 题目:
有\(N\)种物品和容量为\(V\)的背包,第\(i\)种物品最多有\(M_i\)件可用,放第\(i\)种物品的体积是\(C_i\),得到的价值是\(W_i\),问放入背包哪些物品能使价值总和最大,且空间总和不超过背包容量。
3.2 思路:
题目和完全背包类似,但多了些限制,对于第\(i\)种物品,我们的策略数从\(\left \lfloor V/C_i \right \rfloor+1\) 变成了\(M_i+1\) (取0件,取1件,取2件,...,取\(M_i\)件)
我们定义\(F[i,v]\)为前\(i\)种物品恰好放入容量为\(v\)的背包可以得到的最大价值,得到状态转移方程
\(F[i,v]=max\left\{F[i-1,v-kC_i]+kW_i|0\leq k\leq M_i\right\}\)
时间复杂度\(O(V\sum M_i)\)
3.3 优化:
之前我们是吧多重背包用了完全背包的想法来想,那么他能否和01背包联系在一起呢
把第\(i\)种物品换成\(M_i\) 件01背包中的物品,得到了物品数为\(\sum M_i\)的01背包问题,时间复杂度还是\(O(V\sum M_i)\)
但我们仍考虑二进制的思想,我们把第\(i\)种物品换成若干件物品,是的原问题中第\(i\)种物品可取的每一种策略均能用我们分成的若干件物品代替,即\(1,2,2^2,2^3,\dots,2^{k-1},M_i-2^k+1\) ,\(k\)是满足\(M_i-2^k+1>0\)的最大整数
例如\(M_i=13\),\(k=3\) 分成\(1,2,4,6\) 四件物品
这样,原问题的时间复杂度被降为 \(O(V\sum logM_i)\) √
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define clean(a,b) memset(a,b,sizeof(a));
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
const int maxn=1e3+9;
int c[maxn],w[maxn],dp[maxn],m[maxn];
int n,v;
void ZeroOnePack(int c,int w)
{
for(int i=v;i>=c;i--)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
void CompletePack(int c,int w)
{
for(int i=c;i<=v;i++)
{
dp[i]=max(dp[i],dp[i-c]+w);
}
}
int main()
{
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d%d",&c[i],&w[i],&m[i]);
for(int i=1;i<=n;i++)
{
if(m[i]*c[i]>=v) CompletePack(c[i],w[i]);
else
{
for(int k=1;k<m[i];k*=2)
{
ZeroOnePack(k*c[i],k*w[i]);
m[i]-=k;
}
ZeroOnePack(m[i]*c[i],m[i]*w[i]);
}
}
printf("%d\n",dp[v]);
return 0;
}
3.4 多重背包的单调队列优化:
最初的状态转移方程:\(f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i])\)
易得,对于\(j\%c[i]\) 相同的余数得到的位置是互相影响的,余数不同则相互独立
所有我们可以考虑对不同的余数进行分组,相同的分为一组,那么可分\(c[i]\)组
假设\(k\)是一个常数\(f[i][j]\)会影响\(f[i][j-k*c[i]]\)
设\(d=c[i],a=j/c[i],b=j\%c[i]\)
\(j=a*d+b\)
\(j-k*d=a*d+b-k*d=(a-k)*d+b\)
设\((a-k)=k'\)
\(f[i][j]=max(f[i-1][(a-k)*c[i]+b]+k*w[i])\)
\(f[i][j]=max(f[i-1][k'*c[i]+b]+a*w[i]-k'*w[i])\)
\(a*w[i]\) 是一个常量
\(f[i][j]=max(f[i-1][k'*c[i]+b]-k'*w[i])+a*w[i]\)
$k \in [1,lim] $ , \(k'\in[a-k,a]\)
\(lim=V/c[i]\)
最后就是求\((f[i-1][k'*c[i]+b]-k'*w[i])\) 的\(lim+1\)个数的最大值
将\(f[i][j]\)前面所有的\(f[i-1][k'*c[i]+b]-k'*w[i]\) 放入一个队列
为了方便求队列的最大值\(+a*w[i]\) 可以使用单调队列(他不就是维护最大值的吗)
//V=总容量
//c[i] 体积 w[i] 价值 num[i] 数量
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&c[i],&w[i],&num[i]);
if(w[i]*num[i]>=V) //转换为完全背包
{
for(int j=w[i];j<=V;j++)
{
dp[j]=max(dp[j],dp[j-w[i]]+w[i]);
}
continue;
}
for(int mo=0;mo<c[i];mo++)
{
int l=1,r=1;
for(int k=0;k<=(V-mo)/c[i];k++)
{
int now=f[k*c[i]+mo]-k*w[i];
while(l<r&&q[r-1]<=now) r--;
q[r]=now;
pos[r++]=k;
while(l<r&&k-pos[l]>num[i]) l++;
f[k*c[i]+mo]=max(f[k*c[i]+mo],q[l]+k*w[i]);
//滑动区间长度不大于num[i],因为f[k*c[i]+b]-k*w既然存在,那么再加c区间的k*w的值肯定能取到
}
}
}
4 混合背包问题
属于哪种背包就用哪种方法求解即可
for(int i=1;i<=n;i++)
{
if(第i件物品属于01背包) ZeroOnePack(c[i],w[i]);
else if(第i件物品属于完全背包) CompletePack(c[i],w[i]);
else if(第i件物品属于多重背包) MultiplePack(c[i],w[i],m[i]);
}
5 二维费用背包问题
5.1 问题
二维费用背包是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用,对于每种费用都有一个可付出的最大值(背包容量),那么怎样选择物品可以得到最大的价值?
设第\(i\)件物品所需的两种费用分别为\(C_i\) 和\(D_i\) 。两种可付出的最大值(也叫背包容量)分别为\(V\) 和\(U\) ,物品价值维\(W_i\)。
5.2 方法
费用加了一维,状态也加一维就好了,设\(F[i,v,u]\) 表示前\(i\)件物品付出两种费用分别为\(v\)和\(u\)时可获得的最大价值
可得到状态转移方程
$F[i,v,u]=max{F[i-1,v,u],F[i-1,v-C_i,u-D_i]+W_i} $
用之前优化空间的思想,把三维变成二维
当每件物品只取一次 循环逆序,当每件物品可选多次 循环顺序,当每件物品有固定件数时拆分物品 都是一样的
[参考自 崔添翼-背包九讲]