背包问题

背包问题

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背包相比,这样的时间复杂度未免过于大了,我们考虑是否有方法把时间复杂度降下来

  1. 若两件物品\(i,j\) 满足\(C_i\leq C_j\)\(W_i\leq W_j\) ,则可以不用考虑\(j\)

可以先将费用大于\(V\)的去掉,然后去找费用相同的物品,价值最高的那一个

虽然这种优化能大大减少物品的件数,但貌似并不能改善最坏情况下的时间复杂度

  1. 因为每个数都可以得到他的二进制表示,那么每一个问题的可行的答案都可以用满足\(C_i2^k\leq V\) 的非负整数\(k\) (费用为\(C_i2^k\),价值\(W_i2^k\))的物品来表示

这样就可以把每种物品拆成\(O(log\left \lfloor V/C_i \right \rfloor)\) 件物品

  1. 在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} $

用之前优化空间的思想,把三维变成二维

当每件物品只取一次 循环逆序,当每件物品可选多次 循环顺序,当每件物品有固定件数时拆分物品 都是一样的

[参考自 崔添翼-背包九讲]

posted @ 2021-01-31 20:52  L·S·D  阅读(96)  评论(0编辑  收藏  举报