背包问题初探

【前言】

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

【目录】

(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
原文地址:https://www.cnblogs.com/czsharecode/p/9608279.html