点菜问题(北京大学复试上机题)(0-1背包问题)

点菜问题(北京大学复试上机题)(0-1背包问题)

题目描述:

北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为C元,有N种菜可以点,经过长时间的点菜,网络实验室对于每种菜 i 都有一个量化的评价分数(表示这个菜可口程度),为Vi,每种菜的价格为Pi, 问如何选择各种菜,使得在报销额度范围内能使点到的菜的总评价分数最大。
注意:由于需要营养多样化,每种菜只能点一次。

输入格式:

输入的第一行有两个整数C和N,C代表总共能够报销的额度,N代表能点菜的数目。
接下来的N行每行包括两个整数Pi和Vi,分别表示第 i 道菜的价格和评价分数。

输出格式:

输出共一行,一个整数,表示在报销额度范围内,所点的菜能够得到的最大评价分数。

数据范围:

1≤C≤1000,
1≤N≤100,
1≤Pi,Vi≤100

输入样例:输出样例:
90 4
20 25
30 20
40 50
10 18
95

题解(以本题内容作答,和0-1背包问题思路完全一致)

一、二维解法

解题思路:
  • 状态定义:dp(i,j)表示前 i 道菜品,在报销额度为 j 的情况下的最大总评价分数。
  • 分支情况(状态转移方程):对于前 i 道菜品的总评价分数dp(i,j)
    • 不选择第 i 道菜品,那么dp(i,j) = dp(i - 1, j),即和前i - 1道菜的总评价分数一致;
    • 选择第 i 道菜品,那么dp(i,j) = dp(i - 1, j - Pi) + Vi,即为前 i - 1道菜品在报销额度为j - Pi的最大总评价分数第 i 道菜品的评价分数之和
  • 那么什么时候加入第 i 道菜,什么时候不加入第 i 道菜呢?(第 i 道菜的价格记录在price[i - 1],评价分数记录在score[i - 1],报销额度是 j
    • 第一种情况是,当我们第 i 道菜的价格大于报销额度 j 时(即price[i - 1] > j),显而易见,不能加入。
    • 第二种情况是,第 i 道菜的价格小于报销额度(即price[i - 1] < j)时,若加入第 i 道菜后,前 i 道菜的总评价分数大于不加入第 i 道菜时前 i 道菜的总评价分数, 即dp[i - 1, j] < dp[i - 1, j - price[i - 1]] + score[i - 1]时,选择加入。
  • 综上所述,便有核心代码:
if (price[i - 1] > j)
	dp[i][j] = dp[i - 1][j];
else
	dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - price[i - 1]] + score[i - 1]);
AC代码:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

int main()
{
    int c, n; // 报销额度、点菜数目
    cin >> c >> n;
    vector<int> price(n, 0);
    vector<int> score(n, 0);
    for (int i = 0; i < n; i++)
        cin >> price[i] >> score[i];
    vector<vector<int>> dp(n + 1, vector<int>(c + 1, 0)); // dp[i][j]表示前i个菜品在报销额度为j的情况下的总评价分数
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= c; j++)
        {
            if (price[i - 1] > j)
                dp[i][j] = dp[i - 1][j];
            else
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - price[i - 1]] + score[i - 1]); // 不放入、放入
        }
    cout << dp[n][c] << endl;
    return 0;
}
复杂度分析:
  • 时间复杂度:
    • 外层循环遍历 n 种菜,内层循环遍历 c 种报销额度;
    • 总时间复杂度为 O(n×c)。
  • 空间复杂度:
    • 使用了一个大小为 (n+1)×(c+1) 的 dp 数组;
    • 空间复杂度为 O(n×c)。

二、一维解法

解题思路:
  • 本题使用了滚动数组的思想,一维解法在解决问题的思想上与二维解法一致,只不过是使用一个一维数组来代替二维解法中的二维数组,以节省空间开销。通过观察二维解法的解题过程,不难发现,二维表dp[][]的每一行都是根据上一行计算出来的,与且仅与上一行有关系(dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - price[i - 1]] + score[i - 1]))。所以,每次计算用新的一行覆盖掉旧的一行即可。
  • 对于第 i 道菜品,可以选择点菜,也可以选择不点该菜,那么在预算金额为 j 的情况下,dp[j]的值有两种选择:
    • 不加入第 i 道菜,那么dp[j] = dp[j];
    • 加入第 i 道菜,那么dp[j] = dp[j - price[i - 1]] + score[i - 1]第 i 道菜的价格记录在price[i - 1],评价分数记录在score[i - 1],报销额度是 j);
注意事项:
  • 设置一个一维数组dp[c+1],其中dp[j]表示报销额度为 j 时的最大总金额数;
  • 对一维数组dp[]的遍历,要按照逆序遍历,原因:
    • 可以保证每道菜品仅被选择一次;
    • 由于dp[j]值的计算涉及到的dp[j - price[i - 1]],为未更新之前的数据,且其数组下标小于 j ,所以按照逆序访问,可以保证dp[j]的正确更新。
核心代码:
for (int i = 1; i <= n; i++)
        for (int j = c; j >= price[i - 1]; j--)
            dp[j] = max(dp[j], dp[j - price[i - 1]] + score[i - 1]);
  • 变量 i 表示选择前 i 个菜品;
  • j >= price[i - 1] 是因为报销额度小于price[i - 1]时,第 i 个菜品不可加入,此时前 i 个菜品和前 i - 1 个菜品得分一致,无需更新,避免了重复计算。
AC代码:
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int c, n; // 报销额度,点菜数目
    cin >> c >> n;
    vector<int> price(n, 0); // 菜品价格
    vector<int> score(n, 0); // 菜品得分
    for (int i = 0; i < n; i++)
        cin >> price[i] >> score[i];
    vector<int> dp(c + 1, 0);    // dp[i]表示报销额度为i时的菜品得分
    for (int i = 1; i <= n; i++) // i表示选择前i个菜品
        for (int j = c; j >= price[i - 1]; j--) 
            dp[j] = max(dp[j], dp[j - price[i - 1]] + score[i - 1]);
    cout << dp[c] << endl;
    return 0;
}
复杂度分析:
  • 时间复杂度:
    • 外层循环遍历 n 种菜,内层循环遍历 c 种报销额度;
    • 总时间复杂度为 O(n×c)。
  • 空间复杂度:
    • 使用了一个大小为 c+1 的 dp 数组;
    • 空间复杂度为 O( c )。

作者水平有限,不当之处恳请大家指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值