CCF-GESP六级C++真题精讲:用‘01背包’思路搞定‘小杨买饮料’(附完整代码)
当算法竞赛选手第一次看到"小杨买饮料"这道题时,往往会陷入两种极端:要么觉得这不过是个简单的贪心选择问题,要么被题目中"总容量不低于L"的条件吓得手足无措。实际上,这道来自CCF-GESP六级考试的真题完美诠释了动态规划在实际问题中的巧妙应用——它表面上是个购物问题,骨子里却是个标准的01背包变种。
1. 题目本质与算法选择
题目描述看似简单:商店有N种饮料,每种有价格和容量,要求购买总容量≥L的前提下花费最少,且每种饮料最多买一瓶。新手容易犯的错误是直接按"性价比"(每毫升价格)排序后贪心选择,但样例1已经证明这种思路的错误——最优解9元对应的方案(2,4,3元)并非性价比最高的组合。
关键突破点在于识别出三个核心特征:
- 离散决策:每种饮料只有买/不买两种选择
- 累积效应:总容量是各饮料容量的叠加
- 最优子结构:当前决策影响后续状态
这三点正是动态规划(特别是01背包)的典型适用场景。但与经典背包问题不同的是:
| 特征 | 经典01背包 | 小杨买饮料 |
|---|---|---|
| 限制条件 | 容量≤上限 | 容量≥下限 |
| 目标 | 价值最大化 | 花费最小化 |
| 边界处理 | 超容即无效 | 超容仍有效 |
2. 动态规划状态设计
解决这个问题的核心是设计合适的状态表示。我们定义:
dp[j]:获得至少j毫升饮料的最小花费- 初始状态:
dp[0] = 0(0毫升不需要花钱),其余设为无穷大(表示不可达)
状态转移方程需要考虑题目允许"超额满足"的特殊条件:
dp[j] = min(dp[j], dp[max(j - l, 0)] + c);其中max(j - l, 0)的处理是关键——当当前饮料容量l已经满足剩余需求j时,我们直接从dp[0]转移而来。
实现细节注意点:
- 内循环需要倒序枚举j,避免同一饮料被多次选择
- 最终解是
dp[L],若仍为初始值则输出无解 - 数组大小应设为L+1而非饮料总容量上限
3. 完整代码实现与逐行解析
以下是带详细注释的AC代码,严格遵循CCF考试提交规范:
#include <iostream> #include <algorithm> using namespace std; const int INF = 1e9; // 定义足够大的数代表"无穷大" int main() { int N, L; cin >> N >> L; int* dp = new int[L + 1]; // 动态数组更灵活 fill(dp, dp + L + 1, INF); // C++标准库填充函数 dp[0] = 0; // 基础状态初始化 for (int i = 0; i < N; ++i) { int cost, volume; cin >> cost >> volume; // 逆向更新防止重复选择 for (int j = L; j >= 0; --j) { int prev = max(j - volume, 0); dp[j] = min(dp[j], dp[prev] + cost); } } if (dp[L] == INF) { cout << "no solution" << endl; } else { cout << dp[L] << endl; } delete[] dp; // 释放动态数组 return 0; }关键代码段解析:
fill(dp, dp + L + 1, INF):比循环赋值更高效的初始化方式prev = max(j - volume, 0):处理超额满足的核心逻辑j--的遍历顺序:确保每种饮料只被考虑一次- 动态数组管理:适应不同规模的L值
4. 算法复杂度与优化空间
该解法的时间复杂度为O(N*L),空间复杂度O(L)。对于GESP考试的数据规模(通常N,L≤1000)完全足够。但在实际竞赛中,可以考虑以下优化方向:
滚动数组技巧:
vector<int> dp(L + 1, INF); dp[0] = 0; for (auto& drink : drinks) { for (int j = L; j >= 0; --j) { int prev = max(j - drink.vol, 0); dp[j] = min(dp[j], dp[prev] + drink.cost); } }常见错误排查表:
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| 输出结果偏大 | 正序更新导致重复选择 | 改为逆序更新 |
| 部分样例WA | 未处理超额满足条件 | 添加max(j-l,0)处理 |
| 内存超限 | 数组开得过大 | 精确计算L+1空间 |
| 时间超限 | 使用vector未reserve | 预分配内存 |
5. 同类问题拓展与思维训练
掌握这个模型后,可以解决一系列变种问题:
恰好满足版:要求总容量严格等于L
- 修改:初始化时只设dp[0]=0,转移时去掉max处理
多重选择版:每种饮料可买多瓶
- 解法:将内循环改为正序,转化为完全背包问题
双限制条件:同时限制总容量和总瓶数
- 状态设计:dp[j][k]表示j容量k瓶时的最小花费
推荐练习题目:
- LeetCode 416(分割等和子集)
- POJ 3624(经典01背包)
- AtCoder DP Contest D(变种背包)
在实际编程竞赛中,动态规划的难点往往不在于写出状态方程,而在于准确识别问题背后的模型。建议通过以下步骤训练:
- 将问题描述中的关键参数提取为"物品"和"容量"
- 明确决策选项(选/不选,选多少次)
- 确定目标是最小化还是最大化
- 处理特殊边界条件(如本题的超额满足)