3 条题解
-
0
这是一个功能完善的 C++ 数据生成器。它集成了标准题解逻辑(用于生成
.out)和针对不同测试点要求的数据构造逻辑(用于生成.in)。运行一次即可自动生成
1.in/1.out到10.in/10.out,涵盖了 40%、70%、100% 数据范围,以及“无解”、“大容量单品”、“大花费”等边界情况。数据生成器代码 (C++14)
/** * B3873 [GESP202309 六级] 小杨买饮料 - 数据生成器 * 功能:生成 1.in/1.out ~ 10.in/10.out * 作者:OI教练 */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <cstring> #include <string> #include <cstdlib> #include <ctime> using namespace std; // ========================================== // Part 1: 标准题解逻辑 (用于生成 .out) // ========================================== const int INF = 0x3f3f3f3f; const int MAXL_SOLVE = 2005; // 对应 L 的最大值 2000 int dp[MAXL_SOLVE]; struct Drink { int c, l; // cost, volume }; string solve(int n, int target_l, const vector<Drink>& drinks) { // 初始化 DP 数组 memset(dp, 0x3f, sizeof(dp)); dp[0] = 0; for (const auto& drink : drinks) { // 倒序遍历 for (int j = target_l; j >= 0; j--) { if (dp[j] == INF) continue; // 状态转移,超过 L 的都算作 L int next_vol = min(target_l, j + drink.l); if (dp[next_vol] > dp[j] + drink.c) { dp[next_vol] = dp[j] + drink.c; } } } if (dp[target_l] == INF) { return "no solution"; } else { return to_string(dp[target_l]); } } // ========================================== // Part 2: 数据构造工具 // ========================================== // 生成 [min, max] 范围内的随机整数 int randInt(int min, int max) { if (min > max) return min; return rand() % (max - min + 1) + min; } // ========================================== // Part 3: 测试点生成逻辑 // ========================================== void makeData(int fileId) { string inName = to_string(fileId) + ".in"; string outName = to_string(fileId) + ".out"; ofstream fin(inName); ofstream fout(outName); int N, L; vector<Drink> drinks; // ------------------------------------------------- // 40% 数据范围: N <= 20, L <= 100, li <= 100 // ------------------------------------------------- if (fileId == 1) { // 基础测试:小规模,必然有解 N = 5; L = 100; for(int i=0; i<N; i++) drinks.push_back({randInt(1, 10), randInt(20, 50)}); } else if (fileId == 2) { // 基础测试:小规模,构造“无解”情况 // 总容量加起来都不够 L N = 5; L = 100; for(int i=0; i<N; i++) drinks.push_back({randInt(1, 10), randInt(1, 10)}); // sum max 50 < 100 } else if (fileId == 3) { // 边界测试:恰好满足 L N = 10; L = 100; int current_sum = 0; for(int i=0; i<N-1; i++) { int v = randInt(1, 10); drinks.push_back({randInt(1, 10), v}); current_sum += v; } // 最后一个凑够 100 if (current_sum < 100) drinks.push_back({randInt(1, 10), 100 - current_sum}); else drinks.push_back({randInt(1, 10), 1}); } // ------------------------------------------------- // 70% 数据范围: li <= 100 (N, L 变大) // ------------------------------------------------- else if (fileId == 4) { // 中等规模 N = 50; L = 500; for(int i=0; i<N; i++) drinks.push_back({randInt(10, 100), randInt(1, 100)}); } else if (fileId == 5) { // 单个物品体积很小,需要很多个才能凑齐 N = 100; L = 1000; for(int i=0; i<N; i++) drinks.push_back({randInt(1, 5), randInt(1, 10)}); } // ------------------------------------------------- // 100% 数据范围: N <= 500, L <= 2000, ci, li <= 10^6 // ------------------------------------------------- else if (fileId == 6) { // 特殊情况:存在“单瓶容量极大”的饮料 (li >= L) // 测试 min(L, j+v) 逻辑是否正确 N = 10; L = 100; drinks.push_back({10, 2000}); // 一瓶就够,看是否选它 for(int i=0; i<N-1; i++) drinks.push_back({randInt(1, 5), randInt(1, 10)}); } else if (fileId == 7) { // 边界测试:N=1 N = 1; L = 100; // 有解 drinks.push_back({100, 150}); } else if (fileId == 8) { // 边界测试:N=1 无解 N = 1; L = 100; drinks.push_back({100, 50}); } else if (fileId == 9) { // 压力测试:花费很大 (接近 10^6) // 测试是否会溢出 (虽然本题 int 够用,但这是好习惯) N = 500; L = 2000; for(int i=0; i<N; i++) drinks.push_back({randInt(900000, 1000000), randInt(100, 2000)}); } else { // fileId == 10 // 综合压力测试:完全随机大数 N = 500; L = 2000; for(int i=0; i<N; i++) drinks.push_back({randInt(1, 10000), randInt(1, 3000)}); } // 写入输入文件 fin << N << " " << L << endl; for (const auto& d : drinks) { fin << d.c << " " << d.l << endl; } // 计算并写入输出文件 fout << solve(N, L, drinks) << endl; fin.close(); fout.close(); cout << "Generated Case " << fileId << ": N=" << N << ", L=" << L << endl; } int main() { // 初始化随机数种子 srand((unsigned)time(NULL)); for (int i = 1; i <= 10; i++) { makeData(i); } cout << "All 10 test cases generated successfully!" << endl; return 0; }数据分布说明 (符合教练出题标准)
测试点 N L 特征说明 预期考察点 1 5 100 小数据,基础 40% 范围基础逻辑 2 无解 (总容量 < L) no solution判定3 10 恰好凑齐或微量溢出 边界状态转移 4 50 500 中等规模 70% 范围,常规 DP 5 100 1000 物品多,单体容量小 循环次数较多的情况 6 10 100 单品超大 () 考察状态压缩 min(L, j+v)7 1 极限 N=1 (有解) 极小数据边界 8 极限 N=1 (无解) 9 500 2000 大花费 () 考察 dp值是否溢出 (本题 int 够,但测试健壮性)10 全随机大数 100% 范围综合压力测试 你可以直接编译运行这段代码,它会在当前目录下生成符合 OJ 标准的测试数据文件。
-
0
这是一个符合 NOIP C++14 竞赛标准的题解代码。
核心思路
- 模型识别:这是一个 0/1 背包问题 的变种。
- 限制:每个物品(饮料)只能选一次。
- 目标:总容量 的前提下,总花费最小。
- 不同点:普通背包是“体积 求最大价值”,这里是“体积 求最小花费”。
- 状态定义:
- 定义
dp[j]为:获得总容量至少为j毫升时所需的最少花费。 - 状态压缩(关键点):由于我们只关心是否达到了目标 ,所以对于所有超过 的体积,我们都将其视为 。因此数组只需要开到 。
- 定义
- 初始化:
- 求最小值,所以
dp数组初始化为一个极大值(INF),表示“尚未达到该状态”。 dp[0] = 0,表示容量为 0 时花费为 0。
- 求最小值,所以
- 状态转移:
- 遍历每个物品 。
- 倒序遍历
j从 到 (0/1 背包的标准操作,防止重复选取同个物品)。 - 如果不选该物品:状态不变。
- 如果选该物品:当前状态
dp[j]可以转移到dp[min(L, j + v)]。 - 转移方程:
dp[next_v] = min(dp[next_v], dp[j] + c)。
标准代码 (C++14)
/** * 题目:B3873 [GESP202309 六级] 小杨买饮料 * 考点:0/1 背包、动态规划 (求最小代价) * 难度:普及/提高- */ #include <iostream> #include <vector> #include <algorithm> #include <cstring> // 用于 memset using namespace std; // 定义一个极大值,代表无解或初始无穷大 // 0x3f3f3f3f 是竞赛常用的无穷大常数,约为 10^9,相加不易溢出且可用 memset 赋值 const int INF = 0x3f3f3f3f; // 题目中 L 最大为 2000,开 2005 足够 const int MAXL = 2005; int dp[MAXL]; void solve() { int n, target_l; if (!(cin >> n >> target_l)) return; // 关键点1:初始化 DP 数组 // 我们要求的是“最小花费”,所以初始化为无穷大 memset(dp, 0x3f, sizeof(dp)); // 容量为 0 时,花费自然是 0 dp[0] = 0; // 读入每个物品并进行 DP 转移 for (int i = 0; i < n; i++) { int cost, vol; // c_i, l_i cin >> cost >> vol; // 关键点2:0/1 背包必须倒序遍历 // 如果正序遍历,同一个物品可能会被累加多次(变成完全背包) // 我们遍历的是“当前已有的容量状态 j” for (int j = target_l; j >= 0; j--) { // 如果 dp[j] 是 INF,说明还凑不出容量 j,无法从这里转移 if (dp[j] == INF) continue; // 计算加上当前饮料后的新容量 // 关键点3:超过 target_l 的部分都归为 target_l // 这样可以保证数组下标不越界,且符合题目“不低于 L”的要求 int next_vol = min(target_l, j + vol); // 状态转移:取更小的花费 // 这一步使用了 long long 的思维,但由于 cost 和 dp 值都在 int 范围内, // 且 10^9 + 10^6 不会爆 int,所以这里用 int 没问题。 // 题目最大花费估计:500 * 10^6 = 5*10^8 < 2*10^9 (INT_MAX) if (dp[next_vol] > dp[j] + cost) { dp[next_vol] = dp[j] + cost; } } } // 关键点4:输出结果判断 // 如果 dp[target_l] 仍然是 INF,说明无法凑出至少 L 的容量 if (dp[target_l] == INF) { cout << "no solution" << endl; } else { cout << dp[target_l] << endl; } } int main() { // 竞赛标准 IO 优化 ios::sync_with_stdio(false); cin.tie(0); solve(); return 0; }易错点解析
- 数组大小与状态定义:
- 题目中饮料单瓶容量 可达 。如果直接用最大容量开 DP 数组 (
dp[1000000]),不仅空间浪费,而且遍历时间会超时。 - 正解:根据题目要求“不低于 ”,我们将所有 的状态都映射到
dp[L]。这样数组大小仅需 (即 2000 左右),时间复杂度为 ,非常高效。
- 题目中饮料单瓶容量 可达 。如果直接用最大容量开 DP 数组 (
- 循环顺序:
for (int j = target_l; j >= 0; j--)。必须倒序!- 如果正序遍历,当你计算
dp[50]时用了这瓶饮料,稍后计算dp[100]时可能又利用了刚刚更新过的dp[50],导致这瓶饮料被买了两次。
- INF 的选择:
- 使用
0x3f3f3f3f是竞赛老手的习惯。它足够大(约 ),且INF + INF不会溢出int,还支持memset(按字节赋值)。
- 使用
- No Solution 的判断:
- 不要直接输出
dp[L],题目明确要求可能无解。必须判断是否等于初始值。
- 不要直接输出
- 模型识别:这是一个 0/1 背包问题 的变种。
-
0
你好!我是你的OI教练。很高兴看到你在攻克 GESP 六级的题目。这道题是背包问题的一个变种,非常经典,但也藏着几个容易踩的坑。
这道题不再是简单的“容量限制下求最大价值”,而是反过来了:“需求限制下求最小代价”。
我们还是老规矩,拿出草稿纸,把思路一层层剥开。
1. 读题关键词:你的“雷达”响了吗?
读题时,这几个关键词决定了你的算法模型,请务必圈出来:
- “每种饮料至多购买 1 瓶”:
- 核心暗示:这是典型的 0/1 背包问题 模型。每个物品只有拿或不拿两种选择。
- “总容量不低于 ”:
- 状态定义的变化:通常背包问题是“体积不超过 ”,这里是“体积至少为 ”。
- 思考:如果我拿了 20000 毫升,虽然远超 ,但也是满足条件的。所以,所有大于 的体积,其实都可以看作是 (满足了就行)。
- “尽可能少的费用”:
- 目标函数:我们要计算的是
min(cost),而不是max(value)。 - 初始化:求最小值时,数组通常要初始化为“无穷大”,而不是 0。
- 目标函数:我们要计算的是
- “” vs “”:
- 数据范围陷阱: 和 很小,但单个饮料的容量 可能非常大。
- 结论:你的 DP 数组大小应该由 决定,而不是由 的最大值决定。千万不要开 大小的数组,那是浪费空间且容易超时的。
2. 预备知识点
要解决这道题,你需要掌握:
- 0/1 背包问题的滚动数组优化:将二维 DP 压成一维,注意循环顺序是从大到小。
- DP 初始化技巧:求最小值(Min)问题,初始状态设为 INF(一个很大的数),起点的
dp[0]设为 0。 - 边界处理:如何处理体积溢出 的情况。
3. 启发式教学:草稿纸上的推演
我们要建立一个数组
dp,dp[j]表示获得至少j毫升饮料所需的最小花费。(或者理解为:获得正好j毫升,对于超过 的都归入dp[L])。定义状态:
dp[j]:凑出体积 所需要的最少钱数。- 特别地,当体积 时,我们将它统一存放在
dp[L]中。
初始化:
- 我们想求最小值,所以先把
dp数组全填满999999999(表示如果不买饮料,不可能凑出这些体积)。 - 唯独
dp[0] = 0(凑出 0 毫升不需要花钱)。
推导过程(以样例 1 为例): 目标 。 DP 数组大小为 101 (0~100)。
-
第 1 瓶 (花费100, 容量2000):
- 如果我们选它,容量变成 2000。因为 ,我们把它记在
dp[100]上。 dp[100]更新为min(原值, dp[0] + 100) = 100。- 现在:
dp[100]=100, 其他是无穷大。
- 如果我们选它,容量变成 2000。因为 ,我们把它记在
-
第 2 瓶 (花费2, 容量50):
- 我们从大到小遍历 (从 到 )。
- 对于
dp[100]:如果不选是 100;如果选(假设之前有 50 的状态),但之前dp[50]是无穷大,没用。 - 对于
dp[50]:可以由dp[0]转移而来。dp[50] = min(INF, dp[0] + 2) = 2。 - 注意:如果 ,我们要看
dp[100-50]。 - 但是这里有个特殊的**推法(刷表法思维)**可能更容易理解:
- 遍历当前已知的状态 (从 到 )。
- 如果
dp[j]是有效值(不是无穷大),那我们可以加上这瓶饮料。 - 新体积
next_v = j + 50。 - 关键点:如果
next_v超过了 ,就把它当成 。即next_v = min(L, j + 50)。 - 更新:
dp[next_v] = min(dp[next_v], dp[j] + 2)。
让我们用这个逻辑重跑一遍:
步骤 操作 dp数组变化 (只列出关键点) 初始 dp[0]=0, 其他=INF物品1 Cost=100, Vol=2000 只有 dp[0]有效。推向min(100, 0+2000)即dp[100]。
dp[100] = min(INF, 0+100) = 100。物品2 Cost=2, Vol=50 从 倒推到 0。
1.dp[100]有值,推向min(100, 100+50)即dp[100]。dp[100] = min(100, 100+2) = 100(没变)。
2.dp[0]有值,推向min(100, 0+50)即dp[50]。dp[50] = min(INF, 0+2) = 2。物品3 Cost=4, Vol=40 1. 由 dp[100]推dp[100](无更优)。
2. 由dp[50]推dp[90](dp[90]=2+4=6)。
3. 由dp[0]推dp[40](dp[40]=4)。物品4 Cost=5, Vol=30 ...略...
关键:由dp[90](Cost 6) 推dp[100]。
新体积90+30=120 -> 100。
花费6+5=11。dp[100]更新为min(100, 11) = 11。物品5 Cost=3, Vol=20 ...略...
关键:由dp[90](Cost 6) 推dp[100]。
新体积90+20=110 -> 100。
花费6+3=9。dp[100]更新为min(11, 9) = 9。
得到最终答案 9。
4. 核心逻辑梳理 (伪代码思路)
- 定义:
dp数组大小为 (下标 0 到 )。 - 初始化:
dp全为 INF,dp[0] = 0。 - 循环:
- 外层循环遍历每一个饮料 (花费 , 体积 )。
- 内层循环 从 倒序遍历到 。
- 为什么要倒序? 因为这是 0/1 背包,防止同一瓶饮料在同一次外层循环中被累加多次。
- 计算加入这瓶饮料后的体积:。
- 核心转移方程:
dp[t] = min(dp[t], dp[j] + c)。 - 注意:只有当
dp[j]不是 INF 时才转移。
- 结果:
- 检查
dp[L]。 - 如果
dp[L]还是 INF,说明凑不出 ,输出no solution。 - 否则输出
dp[L]。
- 检查
5. 易错点总结
- 数组大小:不要开到 ,开到 (即 2005) 就够了。
- 状态压缩:任何超过 的体积都视为 。代码里要写
min(L, j + v)。 - No Solution:题目说了如果不能满足要输出
no solution。这不仅包括买光了都不够的情况,也包括dp[L]没被更新的情况。 - 数据类型:虽然这题花费加起来不太可能爆
int( 约为 ,int是 ),但为了保险起见,建议开long long,或者 INF 设为0x3f3f3f3f(约 ) 即可。
加油!拿起笔,把那个表格画一画,你就能写出最完美的代码!
- “每种饮料至多购买 1 瓶”:
- 1
信息
- ID
- 14215
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者