CCF-GESP六级C++真题精讲:用‘01背包’思路搞定‘小杨买饮料’(附完整代码)

当算法竞赛选手第一次看到"小杨买饮料"这道题时,往往会陷入两种极端:要么觉得这不过是个简单的贪心选择问题,要么被题目中"总容量不低于L"的条件吓得手足无措。实际上,这道来自CCF-GESP六级考试的真题完美诠释了动态规划在实际问题中的巧妙应用——它表面上是个购物问题,骨子里却是个标准的01背包变种。

1. 题目本质与算法选择

题目描述看似简单:商店有N种饮料,每种有价格和容量,要求购买总容量≥L的前提下花费最少,且每种饮料最多买一瓶。新手容易犯的错误是直接按"性价比"(每毫升价格)排序后贪心选择,但样例1已经证明这种思路的错误——最优解9元对应的方案(2,4,3元)并非性价比最高的组合。

关键突破点在于识别出三个核心特征

  1. 离散决策 :每种饮料只有买/不买两种选择
  2. 累积效应 :总容量是各饮料容量的叠加
  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;
}

关键代码段解析

  1. fill(dp, dp + L + 1, INF) :比循环赋值更高效的初始化方式
  2. prev = max(j - volume, 0) :处理超额满足的核心逻辑
  3. j-- 的遍历顺序:确保每种饮料只被考虑一次
  4. 动态数组管理:适应不同规模的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. 同类问题拓展与思维训练

掌握这个模型后,可以解决一系列变种问题:

  1. 恰好满足版 :要求总容量严格等于L

    • 修改:初始化时只设dp[0]=0,转移时去掉max处理
  2. 多重选择版 :每种饮料可买多瓶

    • 解法:将内循环改为正序,转化为完全背包问题
  3. 双限制条件 :同时限制总容量和总瓶数

    • 状态设计:dp[j][k]表示j容量k瓶时的最小花费

推荐练习题目

  • LeetCode 416(分割等和子集)
  • POJ 3624(经典01背包)
  • AtCoder DP Contest D(变种背包)

在实际编程竞赛中,动态规划的难点往往不在于写出状态方程,而在于准确识别问题背后的模型。建议通过以下步骤训练:

  1. 将问题描述中的关键参数提取为"物品"和"容量"
  2. 明确决策选项(选/不选,选多少次)
  3. 确定目标是最小化还是最大化
  4. 处理特殊边界条件(如本题的超额满足)

更多推荐