题目描述
北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为C元,有N种菜可以点,经过长时间的点菜,网络实验室对于每种菜i都有一个量化的评价分数(表示这个菜可口程度),为Vi,每种菜的价格为Pi, 问如何选择各种菜,使得在报销额度范围内能使点到的菜的总评价分数最大。 注意:由于需要营养多样化,每种菜只能点一次。
输入描述:
输入的第一行有两个整数C(1 <= C <= 1000)和N(1 <= N <= 100),C代表总共能够报销的额度,N>代表能点菜的数目。接下来的N行每行包括两个在1到100之间(包括1和100)的的整数,分别表示菜的>价格和菜的评价分数。
输出描述:
输出只包括一行,这一行只包含一个整数,表示在报销额度范围内,所点的菜得到的最大评价分数。
示例1
输入
90 4
20 25
30 20
40 50
10 18
40 2
25 30
10 8
输出
95
38
思路
这道题是一道典型的0-1背包问题的变形。那么先简单的说一下0-1背包问题。0-1背包问题就是说有n 个物品,它们有各自的重量和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?我们采用自底向上的递推算法。设一个数组dp[i][j],它的意思是前i个物品放进容量为j的背包的最大价值。那么讨论状态转移方程。
- 如果第i个物品无法放进背包,那么明显dp[i][j]就等于dp[i-1][j]
- 如果第i个物品可以放进背包,那么dp[i][j]就等于前i-1个物品放进容量为j-Wi的背包的最大价值加上物品i的价值。(可以把背包容量j想象成两个部分,一个部分放前i-1个物品的部分物品使价值最大,剩下的部分即Wi用来放第i个物品),所以dp[i][j] = dp[i-1][j-Wi] + Vi
有了0-1背包问题的思路,我们就可以用0-1背包问题的思路来解决这道问题。菜品的可口程度就相当于物品的价值,菜品的价格就相当于物品的重量,经费总额最大为C就相当于背包的容量为C。这样就可以用上面的状态转移方程来编程了。(自底向上的递推法,详情可以参考我之前写过的关于动态规划问题的文章)
代码
#include <iostream>
using namespace std;
const int MAXN = 101;
const int MAXC = 1001;
//dp[i][j]前i个物品放进容量为j的背包的最大价值
int dp[MAXN][MAXC];
int weight[MAXN];
int value[MAXN];
int main()
{
//背包最大容量为c n个物品
int c,n;
while(scanf("%d%d",&c,&n)!=EOF){
for(int i = 1; i <= n ; ++i){ //认为i=0表示什么都没有
scanf("%d%d",&weight[i],&value[i]);
}
//初始化dp数组
//背包容量为0则什么都放不下,最大价值也一定为0
for(int i = 0; i <= n;i++){
dp[i][0] = 0;
}
//前0个物品放入背包,不管背包容量为多大,最大价值都为0
for(int j = 0; j <= c;++j){
dp[0][j] = 0;
}
//递推获取dp的每一个值
for(int i = 1; i <= n;i++){
for(int j = 1; j <= c;j++){
//如果第i个物品的重量大于当前背包的重量,那么第i个物品无法放入
if(weight[i] > j){
dp[i][j] = dp[i-1][j];
}else{
//如果第i个物品的重量小于等于当前背包的重量,那么我们可以选择放入或者不放入
//选择两者中较大的就行了
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}
}
}
printf("%d\n",dp[n][c]);
}
return 0;
}
改进
上面算法的空间复杂度为o(n*c)的,因为我们定义的dp数组为dp[n][c],那么有没有办法可以降低空间复杂度呢?
开始思考,由上面的分析我们可以知道dp[i][j]要么等于dp[i-1][j],要么等于dp[i-1][j-Wi]+Vi,只有这两种可能。也就是说,当我们求第i行的dp[i][j]时,只需要使用第i-1行的dp[i][j],而与其他行是无关的。所以,如果我们知道第1行的值dp[i],就可以根据这一行就求出第二行dp[i+1],依次类推,我们就可以求出所有的dp[i][j]。再思考,如果我们知道第一行的dp[i][j],用这一行求出第二行的dp[i][j]后,第一行的dp[i][j]就没有用了,求之后的dp[i][j]也不会用到第一行的值,所以我们完全可以把用第一行求出的第二行的dp[i][j]放在第一行,以节省空间。以此类推,我们可以把所有的dp[i][j]放在第一行内,所以我们就不需要一个二维数组了,只要一个一维数组就够了,这样空间复杂度就可以大大降低,变为o©。
再思考,有了上面的分析我们可以知道,要求第i行dp[i][j],我们只需要第i-1行的dp[i][j],而我们只有一行的存储空间。那么我们先把第i-1行的dp[i][j]全部赋给第i行的dp[i][j],然后用这一行的值求dp[i][j]。如果我们从左往右开始算,那么当我们求dp[i][j]时,它要依赖于dp[i-1,j-Wi],而因为我们是从左往右开始算的,那么可能当我们还没求到dp[i][j],dp[i-1,j-Wi]就已经改变了,那等我们求到dp[i][j]时,它的值纠错了,所以不可以从左往右算,那么只能从右往左算了,这样就可以保证当我们求某个dp[i][j]时他所依赖的值不会先发生变化。
改进后的代码
#include <iostream>
using namespace std;
const int MAXN = 101;
const int MAXM = 1010;
int dp[MAXM];
int weight[MAXN];
int value[MAXN];
int main()
{
//背包最大容量为c n个物品
int c,n;
while(scanf("%d%d",&c,&n)!=EOF){
for(int i = 1; i <= n ; ++i){ //认为i=0表示什么都没有
scanf("%d%d",&weight[i],&value[i]);
}
//初始化dp数组
for(int j = 0; j <= c;++j){
dp[j] = 0;
}
//递推获取dp的每一个值
for(int i = 1; i <= n;i++){
//从右往左
for(int j = c; j >= weight[i];j--){
// 第i的初值刚开始都是第i-1行的值,所以如果j<weight[i],本来要dp[i][j] = dp[i-1][j]的
// 但初值就是dp[i-1][j],那么也就可以省去这一步,j也只需要大于等于weight[i]就行
// if(weight[i] > j){
// dp[i][j] = dp[i-1][j];
// }else{
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
}
printf("%d\n",dp[c]);
}
return 0;
}