背包问题总结

本文基于背包九讲的内容编写,添加了例题和一些自己的想法。

 

一、01背包问题

题目

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路:

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放

 

假设f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。那么容易得到状态转移方程是:

f[i][v] = max{f[i-1][v], f[i-1][v-c[i]]+w[i] }

 

解释一下这个方程:将前i件物品放入容量为v的背包中这个子问题,若只考虑第i件物品的策略(放或者不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

 

最终的结果不一定是f[N][V],而是f[N][0…V]的最大值。因为并没有要求必须把背包的所有空间V都填满。

 

以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。

如果用二维数组的思想来实现,肯定是有一个主循环i=1...N,每次算出来二维数组f[i][0...V]的所有值。那么如果只用一个数组f[0...V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事实上,在每次主循环中我们以v=V...0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。

伪代码:

for i = 1 ... N

       for v = V...0

              f[v] = max( f[v], f[v-c[i]] + w[i])

 

其中f[v] = max{ f[v], f[v-c[i]] }就相当于我们的转换方程

f[i][v] = max{f[i-1][v], f[i-1][v-c[i]] },因为现在的f[v-c[i]] 就相当于原来的f[i-1][v-c[i]]。

 

例1:炉石传说(2017爱奇艺校招笔试题)

时间限制:c/c++语言1000MS;其他语言3000MS

内存限制:c/c++语言65536KB;其他语言589824KB

题目描述:

小明喜欢玩一款叫做炉石传说的卡牌游戏,游戏规则如下,玩家拥有N颗水晶和M张卡牌,每张卡牌的使用会消耗a颗水晶并且造成b的伤害值,请你帮小明算一下该如何使用手上的卡牌,在消耗小于等于N颗水晶的前提下造成最多的伤害值之和。

 

输入:

所有输入均为32位正整数  

第一行N M  

第二行到第M+1行 ai bi  


输出:

对于每个测试实例,要求输出在消耗小于等于N颗水晶的前提下能造成的最多的伤害值之和;每个测试实例的输出占一行。  

 

样例输入:

10 4 

5 7  

2 3  

8 10  

3 4  


样例输出:

14  

 

C++代码:

#include<iostream> 
#include<vector> 
#include<cmath> 
using namespacestd; 
 
int main(){ 
    int n,m; //n颗水晶,m张卡牌 
    while(cin>>n>>m){ 
        vector<int> a;   //消耗的水晶  
        vector<int> b;  //b伤害值  
        for(int i=0;i<m; i++){ 
            int tmp; 
            cin>>tmp; 
            a.push_back(tmp); 
            cin>>tmp; 
            b.push_back(tmp); 
        } //输入数据 
         
        vector<int> dp(n+1,0); 
        for(int i=0;i<m;i++){ 
            for(int j=n;j>=a[i];j--){ 
                dp[j]=max(dp[j],dp[j-a[i]]+b[i]); 
            } 
        } 
         cout<<dp[n]<<endl; 
    } //while 
    return 0; 
} 

例2: hihoCoder #1038:01背包

时间限制:20000ms

单点时限:1000ms

内存限制:256MB

描述

且说上一周的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了!

小Ho现在手上有M张奖券,而奖品区有N件奖品,分别标号为1到N,其中第i件奖品需要need(i)张奖券进行兑换,同时也只能兑换一次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。

提示一:合理抽象问题、定义状态是动态规划最关键的一步

提示二:说过了减少时间消耗,我们再来看看如何减少空间消耗

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第一行为两个正整数N和M,表示奖品的个数,以及小Ho手中的奖券数。

接下来的n行描述每一行描述一个奖品,其中第i行为两个整数need(i)和value(i),意义如前文所述。

测试数据保证

对于100%的数据,N的值不超过500,M的值不超过10^5

对于100%的数据,need(i)不超过2*10^5, value(i)不超过10^3

输出

对于每组测试数据,输出一个整数Ans,表示小Ho可以获得的总喜好值。

样例输入

5 1000

144 990

487 436

210 673

567 58

1056 897

样例输出

2099

 

c++代码:

#include<iostream>
#include<vector>
#include<cmath>
using namespacestd;
 
int main(){
       int n,m;//m张劵,n件奖品
       while(cin>>n>>m){
              vector<int> need;
              vector<int> value;
              int i,j,tmp;
              for(i=0;i<n;i++){  //输入n件奖品的信息
                     cin>>tmp;
                     need.push_back(tmp);
                     cin>>tmp;
                     value.push_back(tmp);
              } //for
             
              vector<int> dp(m+1,0); 
              //dp[i]表示m张劵可以兑换到奖品获得的最大价值
              for(i=0;i<n;i++){
                     for(j=m;j>=need[i];j--){
                            dp[j] = max(dp[j],dp[j-need[i]]+value[i]);
                     }
              }
              cout<<dp[m]<<endl;
       }//while
       return 0;
 
}

二、完全背包问题

题目:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

 

基本思想:

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是取0件、取1件、取2件...等很多种。

 

仍然借用01背包的基本思路,但是加以改进。先做一个很简单有效的优化。若两件物品i、j满足c[i] <= c[j] 且w[i] >= w[j],则将物品j去掉,不用考虑。显然这个优化是正确的,因为在任何情况下都可将价值小费用高的j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。

 

将完全背包问题转化为01背包问题来解,最简单的想法是:考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

 

基本思路的状态转移方程可以等价地变形成这种形式:

f[i][v] = max{f[i-1][v], f[i][v-c[i]]+w[i] }

将这个方程用一维数组实现,伪代码如下:

for i = 1...N

       for v = 0...V

              f[v] = max{ f[v], f[v-c[i]] + w[i]}

 

例1:hihoCoder #1043 : 完全背包

时间限制:20000ms

单点时限:1000ms

内存限制:256MB

描述

且说之前的故事里,小Hi和小Ho费劲心思终于拿到了茫茫多的奖券!而现在,终于到了小Ho领取奖励的时刻了!

等等,这段故事为何似曾相识?这就要从平行宇宙理论说起了………总而言之,在另一个宇宙中,小Ho面临的问题发生了细微的变化!

小Ho现在手上有M张奖券,而奖品区有N种奖品,分别标号为1到N,其中第i种奖品需要need(i)张奖券进行兑换,并且可以兑换无数次,为了使得辛苦得到的奖券不白白浪费,小Ho给每件奖品都评了分,其中第i件奖品的评分值为value(i),表示他对这件奖品的喜好值。现在他想知道,凭借他手上的这些奖券,可以换到哪些奖品,使得这些奖品的喜好值之和能够最大。

提示一:切,不就是0~1变成了0~K

提示二:强迫症患者总是会将状态转移方程优化一遍又一遍

提示三:同样不要忘了优化空间哦!

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第一行为两个正整数N和M,表示奖品的种数,以及小Ho手中的奖券数。

接下来的n行描述每一行描述一种奖品,其中第i行为两个整数need(i)和value(i),意义如前文所述。

测试数据保证

对于100%的数据,N的值不超过500,M的值不超过10^5

对于100%的数据,need(i)不超过2*10^5, value(i)不超过10^3

输出

对于每组测试数据,输出一个整数Ans,表示小Ho可以获得的总喜好值。

样例输入

5 1000

144 990

487 436

210 673

567 58

1056 897

样例输出

5940

 

c++代码:

#include<iostream>
#include<vector>
#include<cmath>
using namespacestd;
 
int main(){
       int n,m; //n件奖品,m张奖券
       while(cin>>n>>m){
              vector<int> need;
              vector<int> value;
              int tmp,i,j,k;
              for(i=0;i<n;i++){
                     cin>>tmp;
                     need.push_back(tmp);
                     cin>>tmp;
                     value.push_back(tmp);
              }//for
              //输入奖品和奖券数据
               
              vector<int> dp(m+1, 0);
              for(i=0;i<n;i++){
                     for(j=need[i];j<=m;j++){
                            dp[j] = max(dp[j],dp[j-need[i]] + value[i]);
                     }
              }//for
             
              cout<<dp[m]<<endl;
             
       }//while
      
       return 0;
      
}

三、多种背包问题

题目:

有N种物品和一个容量为V的背包。第i中物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

 

基本算法:

该题和完全背包问题很类似。基本的方差只需将完全背包问题的方程略微一改即可。因为对于第i种物品有n[i]+1种策略:取0件,取1件......取n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:

f[i][v] = max {f[i-1][v-k*c[i]]+k*w[i] | 0<=k<=n[i] }。复杂度是O(V*∑n[i])。

 

转化为01背包问题

另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为∑n[i]的01背包问题,直接求解,复杂度仍然是O(V*∑n[i])

 

例1:

题目:

物品个数N=3,背包容量V=8,则背包可以装下的最大价值为64

输入:

第一行输入n和v,分别表示物品的个数和背包容量

接下来的n行,每行有3个数值,分别表示物品的重量、物品的价值和物品的个数

输入:

3 8

1 6 10

2 10 5

2 20 2

输出:

64

 

c++代码

#include<iostream>
#include<vector>
#include<cmath>
using namespacestd;
 
int main(){
       int n,v; //n:物品的个数 v:背包容量
       while(cin>>n>>v){
              int tmp,i,j,k;
              vector<int> weight;  //物品的重量
              vector<int> value;  //物品的价值
              vector<int> num;  //物品的个数
              for(i=0;i<n;i++){
                     cin>>tmp;
                     weight.push_back(tmp);
                     cin>>tmp;
                     value.push_back(tmp);
                     cin>>tmp;
                     num.push_back(tmp);
              }//for
              //输入
             
              //f[i][v]:表示把前i件物品放入容量为v的背包中获得的最大收益
             
              //动态申请这样的二维数组
              int** f = new int*[n+1];
              for(i=0;i<=n;i++){
                     f[i] = new int[v+1];
              }
             
              //进行初始化
              for(i=0;i<=n;i++){
                     for(j=0;j<=v;j++)
                            f[i][j] = 0;
              }
      
              //动态规划
              for(i=1;i<=n;i++){
                     for(j=1;j<=v;j++){
                            for(k=0;k<=num[i-1];k++){   //注意存的时候第一个元素的值是从下标为0开始存的,这里是从1开始遍历的,因此[]里面是i-1
                                   if((j-k*weight[i-1])>=0){
                                          f[i][j]= max(f[i][j], f[i-1][j-k*weight[i-1]] + k*value[i-1]);
                                   }else{
                                          break;
                                   }//else
                            }
                     }//for
              }
 
               cout<<f[n][v]<<endl;
               
               for(i=0;i<=n;i++){
                    delete[]f[i];
               }
              delete[] f;
             
       } //while
       return 0;
}

例2:九度题目1455:珍惜现在,感恩生活

时间限制:1 秒

内存限制:128 兆

特殊判题:否

提交:1249

解决:582

题目描述:

为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。请问:你用有限的资金最多能采购多少公斤粮食呢?

输入:

输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。

输出:

对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不光所有的大米,并且经费你可以不用完。每个实例的输出占一行。

样例输入:

1

8 2

2 100 4

4 100 2

样例输出:

400

 

c++代码:

#include<iostream>
#include<vector>
#include<cmath>
using namespacestd;
 
int main(){
       int c; //表示有c组测试用例
       cin>>c;
       while(c){
              int n,m;  //n:经费的金额,m:大米的种类
              cin>>n>>m;
              int tmp,i,j,k;
              vector<int> weight;  //存储大米一袋要花的钱
              vector<int> value;   //存储大米一袋的重量
              vector<int> num;   //存储大米的总袋数
              for(i=0;i<m;i++){
                     cin>>tmp;
                     weight.push_back(tmp);
                     cin>>tmp;
                     value.push_back(tmp);
                     cin>>tmp;
                     num.push_back(tmp);
              } //输入
             
              //动态申请一个二维数组
              int** f = new int*[m+1];
              for(i=0;i<=m;i++){
                     f[i] = new int[n+1];
              }
               
              //初始化
              for(i=0;i<=m;i++){
                     for(j=0;j<=n;j++){
                            f[i][j] = 0;
                     }
              }
             
              //动态规划
             
              for(i=1;i<=m;i++){
                     for(j=1;j<=n;j++){
                            for(k=0;k<=num[i-1];k++){
                                   if((j-k*weight[i-1])>=0){
                                          f[i][j]= max(f[i][j], f[i-1][j-k*weight[i-1]]+k*value[i-1]);
                                   }else{
                                          break;
                                   }
                            }
                     }
              }
              cout<<f[m][n]<<endl;
             
             
              for(i=0;i<=m;i++){
                     delete[] f[i];
              }
              delete[] f;
             
              c--;
       } //while
       return 0;
}

四、混合三种背包问题

问题:

如果将前面三种情况混合在一起,也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?

 

01背包与完全背包的混合

考虑到这两种情况最后给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转换方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。伪代码如下:

for i=1...N

       if 第i件物品是01背包

              for v=V...0

                     f[v] = max{ f[v],f[v-c[i]]+w[i] };

       else if 第i件物品是完全背包

              for v=0...V

                     f[v] = max{ f[v],f[v-c[i]]+w[i] };

 

再加上多重背包

如果再加上有的物品最多可以取有限次,那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。

 

 

五、二维费用的背包问题

问题:

二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问这样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。


待补充...

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值