[USACO08NOV] Buying Hay S
题目解析
它是学习 “最小代价凑出至少 W 重量” 的教科书级题目。
1. 题目简述
Farmer John 想购买干草,他需要 至少
现在有
问:满足 总重量
2. 为什么选这道题?
- 核心目标:这道题是“求最小值”,且限制条件是“至少
”。 - 状态压缩:既然只要达到
就行,那么 磅其实都是“满足条件”的状态。我们可以把所有 的状态都压缩进 。 - 无额外干扰:没有分数规划,没有树形结构,纯粹的 DP。
3. 核心逻辑解析 (01背包 vs 完全背包)
虽然原题是完全背包(干草可以买多份),但我为了配合你的需求,先按 01背包(每种干草只能买一份) 的逻辑来讲,你会发现和你刚才写的代码 完全一致。
状态定义:
初始化:
(凑0重量花0元)。 (求最小值,初始要设为无穷大)。
转移方程 (01背包版 - 倒序):
C++
// 01背包倒序
for (int j = H; j >= 0; j--) {
// 技巧:所有超过 H 的重量,都扔进 dp[H] 这个“桶”里
int target = min(H, j + w[i]);
// 取最小值 min
dp[target] = min(dp[target], dp[j] + c[i]);
}
转移方程 (原题完全背包版 - 正序):
这道 P2918 实际代码只需要把 j 的循环改成正序即可。
4. 代码实现 (针对 P2918 的 AC 代码)
这是 P2918 的代码。注意,为了应对原题“完全背包”的特性,内层循环是正序的。
但请你重点关注 min(H, j + w) 这个压缩技巧,这正是你想要的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* P2918 [USACO08NOV] Buying Hay S
* 核心考点:背包问题 + 状态压缩 (处理“至少为H”的情况)
*/
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int MAXH = 50005;
const int INF = 1e9;
int N, H;
int dp[MAXH]; // dp[j]: 获得至少 j 重量干草的最小花费
int main() {
cin >> N >> H;
// 1. 初始化
// 因为求的是“最小”花费,所以初始化为无穷大
for (int i = 1; i <= H; i++) dp[i] = INF;
dp[0] = 0; // 起点
// 2. 读入并进行 DP
for (int i = 1; i <= N; i++) {
int w, c;
cin >> w >> c; // w:重量, c:价格
// 3. 背包 DP
// 注意:原题是完全背包(可以重复买),所以 j 从 0 开始(正序)
// 如果题目改成 01 背包(只能买一次),这里改成 for(int j = H; j >= 0; j--) 即可
for (int j = 0; j <= H; j++) {
// 【核心技巧】状态压缩
// 只要重量超过 H,就把它归约为 H
int target = min(H, j + w);
// 只有当前状态 j 是可达的,才能转移
if (dp[j] != INF) {
dp[target] = min(dp[target], dp[j] + c);
}
}
}
// 4. 输出
// dp[H] 存的就是“重量 >= H”的最小花费
cout << dp[H] << endl;
return 0;
}
5. 总结这个套路
以后只要看到题目要求:
- “至少” 获得
的重量/体积/分数。 - 求 最小 代价(或者在满足至少
的情况下求最大收益)。
通解模板就是:
- 数组开到
大小。 - 转移时目标下标写成
min(W, j + w)。 即为答案。
这道 P2918 是最纯粹的练习题,快去切了它!🔪
代码
./1.cpp.ENOENT: no such file or directory, open '/home/runner/work/rbook_nunjucks/rbook_nunjucks/problems/luogu/2918/1.cpp'
代码2
另一种风格
这份代码使用了 完全背包 的思路来解决问题,并且使用了一种和我们之前讨论的“状态压缩到
这两种方法都是正确的。这份代码的做法是:既然要凑够至少
下面是详细的注释版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <cstdio>
#include <cstring>
#include <algorithm> // 建议加上这个头文件以使用 min 等函数
// 定义全局变量
// h: 需要购买的干草总量 (目标重量)
// n: 干草的种类数
int h, n;
// p[i]: 第i种干草的重量 (Pounds)
// c[i]: 第i种干草的价格 (Cost)
int p[50010];
int c[50010];
// f[j]: 凑够重量 j 所需要的最小花费
// 数组大小开得比 h 大,因为我们允许重量超过 h
int f[55050];
int main(){
// 读入 n (种类) 和 h (目标重量)
scanf("%d%d", &n, &h);
int i, j;
// 读入每种干草的重量和价格
for (i = 1; i <= n; i++){
scanf("%d%d", &p[i], &c[i]);
}
// 1. 初始化
// 因为要求“最小”花费,所以先把 DP 数组全部初始化为一个很大的数 (0x7f 表示很大)
// 这样在取 min 的时候才会被覆盖
memset(f, 0x7f, sizeof(f));
// 边界条件:凑够 0 重量不需要花钱
f[0] = 0;
// 2. 确定背包容量上限
// 这是一个关键点!
// 题目要求至少 h。如果最后一次选了一个很大的包(比如重量5000),
// 那么总重量可能会达到 h + 5000 左右。
// 所以我们把背包上限设为 h + 5000 (题目中单捆干草最大重量是5000)
int m = h + 5010;
// 3. 开始 DP (完全背包模型)
for(i = 1; i <= n; i++) { // 枚举每种干草
// 枚举当前重量 j
// 注意:这里 j 从 p[i] 开始从小到大枚举 (j++)
// 这代表“完全背包”,意味着每种干草可以被选无限次
for(j = p[i]; j <= m; j++){
// 状态转移方程:
// f[j] = min(f[j], f[j - p[i]] + c[i])
// 如果“当前的最小花费” 比 “之前的重量状态(j-p[i]) 加上这捆干草的花费” 还要大
// 那就更新为更小的花费
if( f[j] > f[j-p[i]] + c[i])
f[j] = f[j-p[i]] + c[i];
}
}
// 4. 寻找答案
// 题目要求重量 >= h。
// 所以我们在 [h, m] 这个范围内扫描,找一个花费最小的作为答案。
int mm = 0x7f7f7f7f; // 初始化为一个很大值
for(i = h; i <= m; i++){
if( mm > f[i])
mm = f[i];
}
// 输出找到的最小花费
printf("%d", mm);
return 0;
}
💡 代码点评与对比
这份代码和你刚才学的“压缩法”有两个主要区别:
-
背包类型:
- P1642 和 P4377 是 01背包(每种只能选一次),所以内层循环是
j--(倒序)。 - 这份代码(P2918 Buying Hay)是 完全背包(每种可以无限选),所以内层循环是
j++(正序)。
- P1642 和 P4377 是 01背包(每种只能选一次),所以内层循环是
-
处理“至少为 H”的方法:
-
方法一(压缩法 - 你之前的理解):
把所有超过
的都算在 里。 优点:数组开得小,只要开到
。 代码特征:min(H, j+w)。
-
方法二(越界法 - 这份代码):
把数组开大一点,一直算到
。最后在这个范围内找最小值。 优点:逻辑直观,不需要处理 min 边界。
代码特征:数组开得大 (m = h + 5000),最后有一个循环 for(i=h; i<=m) 找答案。
-
这份代码写得非常标准,是解决完全背包“至少”问题的经典写法之一!