引入
背包问题,是大多数 oier 在学习动态规划(下文用 dp 代替)的过程中,最先接触到的问题。它看似简单,有蕴含着无穷的变化。本文便对作者接触过的背包问题做一个总结。
背包问题,一般情况下指:你有 \(n\) 个物品和一个容量为 \(m\) 的背包,每个物品有重量 \(w\) 和价值 \(v\),各个物品间可能存在一些限制。请选择合适的方案使得在背包承载范围内获得最大的价值。
01背包
01背包,是最早接触到的背包问题。它是指每个物品只有 1 个。
暴力做法是直接搜索当前背包选还是不选,时间复杂度 \(\mathcal O(2^n)\)。
考虑 dp。设 \(f_{i,j}\) 表示到了第 \(i\) 个物品,当前背包总重量为 \(j\) 的方案数。那么有
\[f_{i,j}=\begin{cases}f_{i-1,j}&j<w_i\\\max(f_{i-1,j},f_{i-1,j-w_i}+v_i)&j\geqslant w_i\end{cases} \]时间复杂度 \(\mathcal O(nm)\),空间复杂度 \(\mathcal O(nm)\)。
考虑对空间进行优化。注意到 \(f_i\) 都是由 \(f_{i-1}\) 转移得到,因此可以将这一维去掉,改为 \(f_j=\max(f_j,f_{j-w_i}+v_i)\)。
但注意,此时的 \(j\),应该从 \(m\) 倒叙循环至 \(w_i\)。因为 \(f_j\) 可能会从 \(f_{j-w_i}\) 转移过来,所以要保证在 \(f_j\) 未被更新前,\(f_{j-w_i}\) 仍然是循环至 \(i-1\) 时的 \(f_{j-w_i}\)。这是因为每个物品只能被选一次导致的。如果正序循环,就可能出现 \(f_{j-k\times w_i}\rightarrow f_{j-(k-1)\times w_i}\rightarrow \cdots \rightarrow f_{j-w_i}\rightarrow f_j\) 的情况,这显然是不合法的。
至此,空间复杂度便被优化至 \(\mathcal O(m)\)。
最终答案是 \(f_m\)。
#include<cstdio>
#include<algorithm>
#define M 1005
using namespace std;
int n,m,f[M];
int main()
{
scanf("%d%d",&m,&n);
for (int i=1;i<=n;++i)
{
int w,v;
scanf("%d%d",&w,&v);
for (int j=m;j>=w;--j)//倒序循环
f[j]=max(f[j],f[j-w]+v);
}
printf("%d\n",f[m]);
return 0;
}
完全背包
完全背包与01背包类似,只是此时物品可以选无限个。而完全背包的代码和01背包的也很类似,只是在枚举 \(j\) 的时候由倒叙改为正序,因为此时上文所说的 \(f_{j-k\times w_i}\rightarrow f_{j-(k-1)\times w_i}\rightarrow \cdots \rightarrow f_{j-w_i}\rightarrow f_j\) 这种不合法的情况变成了合法。因此采用正序循环。时空复杂度同样分别为 \(\mathcal O(nm)\) 和 \(\mathcal O(m)\)。
例题:P1616 疯狂的采药
#include<cstdio>
#include<algorithm>
#define M 10000005
#define ll long long
using namespace std;
int n,m;
ll f[M];
int main()
{
scanf("%d%d",&m,&n);
for (int i=1;i<=n;++i)
{
int w,v;
scanf("%d%d",&w,&v);
for (int j=w;j<=m;++j)//正序循环
f[j]=max(f[j],f[j-w]+(ll)v);
}
printf("%lld\n",f[m]);
return 0;
}
多重背包
多重背包位于01背包和完全背包间,此时物品会新增一个 \(t_i\) 表示该物品最多能选多少次。
朴素做法是暴力枚举当前物品选多少个,然后同样是倒序循环。
for (int i=1;i<=n;++i)
{
int w,v,t;
scanf("%d%d%d",&w,&v,&t);
for (int j=m;j>=w;--j)
for (int k=1;k<=t;++l)
f[j]=max(f[j],f[j-k*w]+k*v);
}
但这个做法不优秀,时间复杂度 \(\mathcal O(nmt)\)。而优化有两种,分别为二进制优化和单调队列优化。
二进制优化
我们注意到01背包的复杂度是较为优秀的,考虑将多重背包转换成01背包。
自然的想法是选 \(t\) 个变为 \(t\) 个同样的物品。可复杂度没有改变。
换种思路。我们知道任何一个正整数 \(x\),都可以表示成 \(2\) 的若干次方相加。此时最多有 \(\log_2 x\) 项。根据这个方向,我们考虑将 \(t\) 变为 \(2^0+2^1+\cdots +2^k+x\),这仍然不会超过 \(\log_2 t\) 项。而此时,我们可以任意选择这些项中的某些项,从而得到 \(0\sim t\) 中的任意一个数字。那么我们将原本最多选 \(t\) 次的物品改为这些最多选一次的物品,便完成了对最多选 \(t\) 次的拆分,将多重背包变为01背包。
例如,一个价值为 \(v\),重量为 \(w\),最多选 \(12\) 从的物品,可以拆分以下 \(4\) 个物品:
- 价值为 \(v\),重量为 \(w\) 的物品。
- 价值为 \(2v\),重量为 \(2w\) 的物品。
- 价值为 \(4v\),重量为 \(4w\) 的物品。
- 价值为 \(5v\),重量为 \(5w\) 的物品。
然后进行01背包的操作即可。时间复杂度 \(\mathcal O(nm\log t)\)。
例题:P1776 宝物筛选
#include<cstdio>
#include<algorithm>
#define N 2005
#define M 40005
using namespace std;
int n,m,num,ans,f[M];
struct node {int w,v;}a[N];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i)
{
int v,w,t;
scanf("%d%d%d",&v,&w,&t);
for (int j=0;j<17;++j)
if (t>=(1<<j))
{
a[++num].v=v*(1<<j);
a[num].w=w*(1<<j);
t-=(1<<j);
}
if (t) a[++num].v=v*t,a[num].w=w*t;
}
for (int i=1;i<=num;++i)
for (int j=m;j>=a[i].w;--j)
f[j]=max(f[j],f[j-a[i].w]+a[i].v);
printf("%d\n",f[m]);
return 0;
}
单调队列优化
我们可以进一步优化。对于每个物品,它每选一次只会更改 \(w\) 的重量。那不妨枚举余数 \(d\),然后枚举 \(f_{d+j\times w}\),用 \(f_{d+k\times w}+(j-k)\times v(j-k<t)\)。用单调队列维护 \(f_{d+k\times w}-k\times v\) 即可。
#include<cstdio>
#include<algorithm>
#define M 40005
using namespace std;
int n,m,res,ans,hd,tl,q[M],q2[M],f[M];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i)
{
int v,w,t;
scanf("%d%d%d",&v,&w,&t);
if (w==0) {res+=v*t;continue;}//没有重量直接选
t=min(t,m/w);
for (int d=0;d<w;++d)
{
hd=1;tl=0;
for (int j=0;j<=(m-d)/w;++j)
{
while (hd<=tl&&f[d+j*w]-j*v>=q2[tl]) --tl;
q[++tl]=j;q2[tl]=f[d+j*w]-j*v;
while (hd<=tl&&q[hd]<j-t) ++hd;
f[d+j*w]=max(f[d+j*w],q2[hd]+j*v);
}
}
}
for (int i=1;i<=m;++i)
ans=max(ans,f[i]);
printf("%d\n",ans+res);
return 0;
}
标签:背包,浅谈,int,复杂度,01,物品,include
From: https://www.cnblogs.com/Livingston/p/17623842.html