本文基于背包九讲的内容编写,添加了例题和一些自己的想法。
一、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
#include
#include
using namespacestd;
int main(){
int n,m; //n颗水晶,m张卡牌
while(cin>>n>>m){
vector a; //消耗的水晶
vector b; //b伤害值
for(int i=0;i>tmp;
a.push_back(tmp);
cin>>tmp;
b.push_back(tmp);
} //输入数据
vector dp(n+1,0);
for(int i=0;i=a[i];j--){
dp[j]=max(dp[j],dp[j-a[i]]+b[i]);
}
}
cout<
例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
#include
#include
using namespacestd;
int main(){
int n,m;//m张劵,n件奖品
while(cin>>n>>m){
vector need;
vector value;
int i,j,tmp;
for(i=0;i>tmp;
need.push_back(tmp);
cin>>tmp;
value.push_back(tmp);
} //for
vector dp(m+1,0);
//dp[i]表示m张劵可以兑换到奖品获得的最大价值
for(i=0;i=need[i];j--){
dp[j] = max(dp[j],dp[j-need[i]]+value[i]);
}
}
cout<
二、完全背包问题
题目:有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]}
时间限制: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
#include
#include
using namespacestd;
int main(){
int n,m; //n件奖品,m张奖券
while(cin>>n>>m){
vector need;
vector value;
int tmp,i,j,k;
for(i=0;i>tmp;
need.push_back(tmp);
cin>>tmp;
value.push_back(tmp);
}//for
//输入奖品和奖券数据
vector dp(m+1, 0);
for(i=0;i
三、多种背包问题
题目:
有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
#include
#include
using namespacestd;
int main(){
int n,v; //n:物品的个数 v:背包容量
while(cin>>n>>v){
int tmp,i,j,k;
vector weight; //物品的重量
vector value; //物品的价值
vector num; //物品的个数
for(i=0;i>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<
例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
#include
#include
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 weight; //存储大米一袋要花的钱
vector value; //存储大米一袋的重量
vector num; //存储大米的总袋数
for(i=0;i>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<
四、混合三种背包问题
问题:
如果将前面三种情况混合在一起,也就是说,有的物品只可以取一次(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]。
待补充...