点菜问题(北京大学复试上机题)(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 道菜呢?(第 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]时,选择加入。
- 第一种情况是,当我们第 i 道菜的价格大于报销额度 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]);
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);
- 不加入第 i 道菜,那么
注意事项:
- 设置一个一维数组
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 )。
作者水平有限,不当之处恳请大家指正!

737

被折叠的 条评论
为什么被折叠?



