• 背包问题初探


    【前言】

     背包问题是动态规划中的经典问题 ,特此总结三种背包问题的算法原理和一些基本实现,并且对每个问题提出了优化方案。目前只总结到初学者水平,以后拜读《背包九讲》之后如有新的体会再进行补充提升。欢迎读者批评指正。

    【目录】

    (1)0 - 1背包问题

    (2)完全背包问题

    (3)多重背包问题

    【正文】

    一、0 - 1背包

      1.问题概述:有N种物品,每种物品只有一件,第i种物品的体积为c[i] , 价值为w[i] ,现有一个总体积为V的背包,求放入哪些物品能够使背包获得的体积最大。

      2.问题分析:首先考虑贪心,一个容易想到的是如果我们按照 价值/体积 对物品进行排序,然后贪心地取前K种物品。但是这种策略对不对呢,显然是错误的。因为没有充分考虑到背包的容积特性,它是一个确切的数字,如果按照上述策略放物品,有可能使得背包还有大量未填充的空间。举个栗子,背包空间是8,物品有(3,3)(4,2)(3,3)(5,4),(x,y)代表价值为x,体积为y。怎么放呢?按照贪心,则先放(4,2) 再放(5,4),然后发现再也放不下了。这样得到的总价值是4+5 = 9,背包剩余空间是2。显然我们有一种更优的解决办法:放入(3,3)(4,2)(3,3),这样得到总价值为3+4+3 = 10,背包剩余空间是0.

      因此我们需要用动态规划的眼光来解决这个问题,首先我们容易想到,如果先对前i件物品进行选择,dp[i][j]表示前i种物品装入体积为j的背包所能获得的最大值,那么我们最终的目的就是求dp[N][V]

      状态转移方程:dp[i][j] = max{ dp[i-1][ j - c[i] ] + w[i] , dp[i-1][j] }    也就是说对于第i件物品我们要么选要么不选,取最终价值最大的那个。

      注:这里的i 从 1~N j可以从0~V 也可以从V~0

      3.【AC代码】正向 vs 逆向

      HDU 2602 Bone Collector 模板题 

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int n,v;
    int dp[maxn][maxn];
    int main(){
        int t;
        cin>>t;
        while(t--){
            cin>>n>>v;
            memset(dp, 0 ,sizeof dp);
            for(int i=1; i<=n; i++){
                cin>>w[i];
            }
            for(int i=1; i<=n; i++){
                cin>>c[i];
            }
            //逆推,从背包空间为V开始 
            for(int i=1; i<=n; i++){
                for(int j=v; j>=0; j--){
                    if(j >= c[i])
                        dp[i][j] = max( dp[i-1][j], dp[i-1][j-c[i]] + w[i]);
                    else
                        dp[i][j] = dp[i-1][j];
                }
            }
            
            cout<<dp[n][v]<<endl;
        }
    }
    View Code

      

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int n,v;
    int dp[maxn][maxn];
    int main(){
        int t;
        cin>>t;
        while(t--){
            cin>>n>>v;
            memset(dp, 0 ,sizeof dp);
            for(int i=1; i<=n; i++){
                cin>>w[i];
            }
            for(int i=1; i<=n; i++){
                cin>>c[i];
            }
            
            for(int i=1; i<=n; i++){
                //正向递推,背包体积从0开始到V 
                for(int j=0; j<=v; j++){
                    if(j-c[i] >= 0)
                        dp[i][j] = max( dp[i-1][j], dp[i-1][j-c[i]] + w[i]);
                    else
                        dp[i][j] = dp[i-1][j];
                }
            }
            
            cout<<dp[n][v]<<endl;
        }
    }
    View Code

      4. 空间优化:我们可以注意到,如果把dp二维数组画成一个矩阵,那么求dp[i][j]的时候,我们其实只用到了它头顶上的一个元素dp[i-1][j],以及头顶上元素的左边某一个元素dp[i-1][j-c[i]],这两个元素都在dp[i][j]头顶上的那一行,逐渐往下递推的时候其实每次用的都是它头顶上的那一行,那么我们可不可以只用一维数组并且不断更新这个数组呢?答案是可以的,这个数组还有一个名字,叫滚动数组。也就是说我们用的数组始终保存的是“它头顶上的那一行”,不断覆盖。设dp[i]表示背包体积为i时最多能获得的价值,我们最终的目的是求的dp[N]。

      需要注意的是,用滚动数组求dp[i][j]时会发生覆盖,那么什么时候才能覆盖那个元素呢,那就是那个元素已经没有用了,所以我才能覆盖它,那么它什么时候没有用了呢?首先我们考虑它的用处,它的用处是求它脚底下一行的元素,以及求它脚底下右边某处的元素,当这两个元素都已经被求出来的时候,它就没用了。按照这样的思想,我们只能从右往左倒着求dp[i],先把右边的元素求出来。

      图解:

    【空间优化后的代码】

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int n,v;
    int dp[maxn];//dp[i]表示背包体积为i时获得的最大价值 
    int main(){
        int t;
        cin>>t;
        while(t--){
            cin>>n>>v;
            memset(dp, 0 ,sizeof dp);
            //读入价值 
            for(int i=1; i<=n; i++){
                cin>>w[i];
            }
            //读入代价 
            for(int i=1; i<=n; i++){
                cin>>c[i];
            }
            
            for(int i=1; i<=n; i++){
                for(int j=v; j>=c[i]; j--){
                   dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
                }
            }
            
            cout<<dp[v]<<endl;
        }
    }
    View Code

    二、完全背包

    1.问题概述:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求如何放置物品到背包中使得获得的价值最大,最大价值是多少。

    2.问题分析: 完全背包问题和0-1背包问题的区别在0-1背包问题中所有物品只有一件,或者说只能取一次,而完全背包中所有种类的物品均可以取无限次,次数k的范围是 [0, V/c[i] ] 那么递推方程就转化为:

    dp[i][j] = max( dp[i-1][j], dp[i-1][ j - k*c[i] ] + k*w[i] | 0=< k*c[i] <= j)

    当然,k的范围是可求的所以也可以把它转化成0-1背包问题,那就是第种物品有Ki件,每件一毛一样,每件只能被取一次,多加一层K循环即可,但是这样一来复杂度相当高。

    3.空间优化:那么能不能继续使用滚动数组呢?

    回想一下,0-1背包问题为什么要逆序循环,因为在算下一次的 i 的 dp[j]时,必须先算右边的(即j大的)以解决覆盖问题。每次求dp[v]时,使用的是dp[v - c[i]],这是上一轮所算出来的,不是这一轮算出来的所以保证了每一轮算dp[v]时使用的都是上一轮的结果,也就保证了每件物品在每轮计算中都只被选一次。如果我们正序计算dp[v], 则算dp[v]时使用的dp[v - c[i]]是这一轮的,也就是第i件物品又被选了一次,它可以被选择多次。

    其实也可以这样理解,在第一轮中,背包中放的都是第一种物品,放了0件,1,件,2件...... V/c[1]件的结果都知道;第二轮时,在第一轮的基础上,放入若干第二种物品,更新dp[i]使其暂时最优,一直放到最后一种,不断更新dp[i]使其最优。

    【代码】

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int n,v;
    int dp[maxn];//dp[i]表示背包体积为i时获得的最大价值 
    int main(){
        int t;
        cin>>t;
        while(t--){
            cin>>n>>v;
            memset(dp, 0 ,sizeof dp);
            //读入价值 
            for(int i=1; i<=n; i++){
                cin>>w[i];
            }
            //读入代价 
            for(int i=1; i<=n; i++){
                cin>>c[i];
            }
            //正序递推 
            for(int i=1; i<=n; i++){
                for(int j=c[i]; j<=v; j++){
                   dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
                }
            }
            
            cout<<dp[v]<<endl;
        }
    }
    View Code

    三、多重背包

    1.问题概述:有N种物品和一个体积为V的背包,第i种物品最多有n[i]件,花费为c[i], 价值为w[i], 问怎样放使得背包总价值最大?输出最大总价值。

    2.问题分析: 多了一个限制条件,每种物品最多有n[i]件,其实不还是和完全背包一样,完全背包每件物品最多V/c[i]个,这里是给定的n[i]个。

    dp[i][j] = max( dp[i-1][j], dp[i-1][ j - k*c[i] ] + k*w[i] | 0=< k*c[i] <= min(n[i] , V/c[i]))

    或者这样说,把这n[i]件物品看做是n[i]种物品,每种物品只能取一次,这样不就和0-1背包一毛一样了吗?

    3.例题:庆功宴 http://ybt.ssoier.cn:8088/problem_show.php?pid=1269

    【AC代码】

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int s[maxn]; 
    int n,v;
    int dp[6*maxn];//dp[i]表示背包体积为i时获得的最大价值 
    int main(){
        //int t;
        //cin>>t;
        while(cin>>n>>v){
            memset(dp, 0 ,sizeof dp);
            //读入价值 
            for(int i=1; i<=n; i++){
                cin>>c[i]>>w[i]>>s[i];
               }
    
            for(int i=1; i<=n; i++){
                for(int j=v; j>=c[i]; j--){
                    for(int k=1; k<=s[i]; k++){
                        if(j >= k*c[i]){
                            dp[j] = max(dp[j-k*c[i]] + k*w[i], dp[j]);
                        }
                        else
                            break;
                    }
                }
            }
            cout<<dp[v]<<endl;
        }
    }
    View Code

    4.二进制优化

    如果数据规模比较大,那么用K循环枚举物品被放入0个到n[i]个的情况时间复杂度比较高,可以采用二进制压缩的方法,其实相当于一种倍增。

    比如说有一件物品价值为3,数量为10,可以把10进行二进制拆分1,2,4,3。这4个数能够组成【1,10】内任意一个数,并且组合成的所有数恰好填满此区间,也就说不可能组合成除此区间外的任何一个数,这就保证了问题的性质不变。

    推广到一般化,拆分方法为 把 n[i] 拆分成 20 21 22 2......2k  , t初始值为 n[i]  每拆一个就更新t = t - 2k  最后不能再拆时满足 t  <  2k+1  

    【AC代码】

    //二进制优化
    #include<iostream>
    #include<cstring>
    using namespace std;
    
    const int maxn = 1e3+10;
    int w[maxn];
    int c[maxn];
    int s[maxn];
    int n,v;
    int cnt;//二进制转化 后的物品数量 
    int dp[6*maxn];//dp[i]表示背包体积为i时获得的最大价值 
    int main(){
        int t;
        int price, value, quantity; 
        while(cin>>n>>v){
            cnt = 0;
            memset(dp, 0 ,sizeof dp);
            //读入价值 
            for(int i=1; i<=n; i++){
                cin>>price>>value>>quantity;
                
                //进行二进制优化 转化成0-1背包 
                int t = 1;
                while(quantity >= t){
                    c[++cnt] = price * t;
                    w[cnt] = value * t;
                    quantity -= t;
                    t*=2;
                }
                
                //别忘了最后可能有剩余
                if(quantity > 0) {
                    c[++cnt] = price * quantity;
                    w[cnt] = value * quantity;
                }
            }
            //注意i的上限是cnt不是n了 
            for(int i=1; i<=cnt; i++){
                for(int j=v; j>=c[i]; j--){
                   dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
                }
            }
            
            cout<<dp[v]<<endl;
        }
    }
    View Code
  • 相关阅读:
    poj 1753 Flip Game
    SDIBT 2345 (3.2.1 Factorials 阶乘)
    HDU 1176 免费馅饼
    HDU 1058 Humble Numbers
    HDU 1003 MAXSUM(最大子序列和)
    HDU1864 最大报销额
    HDU 1114 Piggy-Bank(完全背包)
    POJ 3624 Charm Bracelet
    处理textarea里Enter(回车换行符)
    uniApp打卡日历
  • 原文地址:https://www.cnblogs.com/czsharecode/p/9608279.html
Copyright © 2020-2023  润新知