点菜问题

题目描述

北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为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的背包的最大价值。那么讨论状态转移方程。

  1. 如果第i个物品无法放进背包,那么明显dp[i][j]就等于dp[i-1][j]
  2. 如果第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;
}

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这道题目是一道经典的背包问题,要求从给定的 n 种菜品中选出若干个菜品,使得它们的价格之和恰好为 m。 我们可以使用动态规划的方法来解决这个问题。具体来说,我们可以定义一个二维数组 f[i][j],表示从前 i 种菜品中选,总价值恰好为 j 的方案数。初始状态为 f[0][0] = 1,表示从 0 种菜品中选出总价值为 0 的方案数为 1(即不选任何菜品)。 然后,我们可以使用状态转移方程 f[i][j] = f[i-1][j] + f[i-1][j-a[i]],表示要么不选第 i 种菜品,此时方案数为 f[i-1][j];要么选第 i 种菜品,此时方案数为 f[i-1][j-a[i]],因为选了这个菜品后,剩余的价值就是 j-a[i]。 最后,我们输出 f[n][m],即从 n 种菜品中选出总价值恰好为 m 的方案数。 下面是 AC 代码和一些细节处理的实现建议: ```c++ #include <iostream> #include <cstring> using namespace std; const int MAXN = 105; const int MAXM = 10005; int f[MAXN][MAXM]; // f[i][j] 表示从前 i 种菜品中选,总价值恰好为 j 的方案数 int a[MAXN]; // a[i] 表示第 i 种菜品的价格 int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; } memset(f, 0, sizeof(f)); // 初始化为 0 f[0][0] = 1; // 初始状态 for (int i = 1; i <= n; i++) { for (int j = a[i]; j <= m; j++) { // 注意这里要从 a[i] 开始枚举 f[i][j] = f[i-1][j] + f[i-1][j-a[i]]; // 状态转移方程 } } cout << f[n][m] << endl; // 输出最终答案 return 0; } ``` 需要注意的细节有: 1. 状态转移方程中,第二个下标 j 要从 a[i] 开始枚举,因为如果 j < a[i],则选第 i 种菜品的话,总价值就会小于 a[i],不符合题意。 2. 初始状态要赋值为 1,因为从 0 种菜品中选出总价值为 0 的方案数只有一种(即不选任何菜品)。 3. 可以使用 memset 函数将 f 数组初始化为 0。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值