zoukankan      html  css  js  c++  java
  • 背包问题回顾

    背包问题

    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
    ",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
    ",dp[v]);
        return 0;
    }
    

    1.4 一些细节

    ​ 在模板题中,题目并没有对是否要装满背包做出要求,但在一些其他的问题中会要求“恰好装满背包”的最优解,而区别在于初始化

    ​ 如果是恰好装满背包,那除了(dp[0]=0)外,(dp[i]=-inf (iin[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 ight floor)件)

    ​ 如果仍用01背包的想法,把每种物品取多少,想成是每种物品的每一件我取还是不取

    ​ 我们定义(F[i,v])为前(i)种物品恰好放入容量为(v)的背包可以得到的最大价值,得到状态转移方程

    (F[i,v]=maxleft{F[i-1,v-kC_i]+kW_i|0leq kC_ileq v ight})

    ​ 01背包的时间复杂度为(O(NK)) ,每种物品只有两个状态,完全背包每种物品有(left lfloor V/C_i ight floor+1) 个状态,时间复杂度为(O(NKsumfrac{V}{C_i}))

    2.3 试着优化下

    ​ 与01背包相比,这样的时间复杂度未免过于大了,我们考虑是否有方法把时间复杂度降下来

    1. 若两件物品(i,j) 满足(C_ileq C_j)(W_ileq W_j) ,则可以不用考虑(j)

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

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

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

    这样就可以把每种物品拆成(O(logleft lfloor V/C_i ight floor)) 件物品

    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]=maxleft{F[i-1,v],F[i,v-C_i]+W_i ight})

    ​ 我们是否能给他一个合理的解释呢

    ​ 确实,还是最初的那个取还是不取的问题,取?取(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
    ",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 ight floor+1) 变成了(M_i+1) (取0件,取1件,取2件,...,取(M_i)件)

    我们定义(F[i,v])为前(i)种物品恰好放入容量为(v)的背包可以得到的最大价值,得到状态转移方程

    (F[i,v]=maxleft{F[i-1,v-kC_i]+kW_i|0leq kleq M_i ight})

    时间复杂度(O(Vsum M_i))

    3.3 优化:

    之前我们是吧多重背包用了完全背包的想法来想,那么他能否和01背包联系在一起呢

    把第(i)种物品换成(M_i) 件01背包中的物品,得到了物品数为(sum M_i)的01背包问题,时间复杂度还是(O(Vsum 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(Vsum 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
    ",dp[v]);
        return 0;
    }
    

    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} $

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

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

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

  • 相关阅读:
    面向对象、面向接口、面向方法编程的区别?
    面向接口、对象、方面编程区别 -- 精简版
    面向接口编程详解(一)——思想基础
    吴裕雄--天生自然数据结构:静态链表及其创建
    吴裕雄--天生自然数据结构:单链表的基本操作
    吴裕雄--天生自然数据结构:单链表,链式存储结构
    吴裕雄--天生自然数据结构:顺序表的基本操作
    吴裕雄--天生自然Python Matplotlib库学习笔记:matplotlib绘图(2)
    吴裕雄--天生自然Python Matplotlib库学习笔记:matplotlib绘图(1)
    吴裕雄--天生自然Numpy库学习笔记:NumPy Matplotlib
  • 原文地址:https://www.cnblogs.com/YangKun-/p/14353970.html
Copyright © 2011-2022 走看看