Loading

浅谈背包

引入

背包问题,是大多数 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\)

例题:P1048 [NOIP2005 普及组] 采药

#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\) 个物品:

  1. 价值为 \(v\),重量为 \(w\) 的物品。
  2. 价值为 \(2v\),重量为 \(2w\) 的物品。
  3. 价值为 \(4v\),重量为 \(4w\) 的物品。
  4. 价值为 \(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);
            }
        }
    }
    printf("%d\n",f[m]+res);
    return 0;
}

posted @ 2023-08-11 19:48  Thunder_S  阅读(4)  评论(0编辑  收藏  举报