题目解析

它是学习 “最小代价凑出至少 W 重量” 的教科书级题目。

1. 题目简述

Farmer John 想购买干草,他需要 至少 HH 磅干草。

现在有 NN 种干草,第 ii 种的重量是 PiP_i,价格是 CiC_i

问:满足 总重量 H\ge H 的前提下,最小 的花费是多少?

2. 为什么选这道题?

  • 核心目标:这道题是“求最小值”,且限制条件是“至少 HH”。
  • 状态压缩:既然只要达到 HH 就行,那么 H+1,H+100,H+1000H+1, H+100, H+1000 磅其实都是“满足条件”的状态。我们可以把所有 H\ge H 的状态都压缩进 dp[H]dp[H]
  • 无额外干扰:没有分数规划,没有树形结构,纯粹的 DP。

3. 核心逻辑解析 (01背包 vs 完全背包)

虽然原题是完全背包(干草可以买多份),但我为了配合你的需求,先按 01背包(每种干草只能买一份) 的逻辑来讲,你会发现和你刚才写的代码 完全一致

状态定义:

dp[j]dp[j] 表示凑出重量 恰好为 jj (当 j=Hj=H 时表示 H\ge H) 的 最小花费。

初始化

  • dp[0]=0dp[0] = 0 (凑0重量花0元)。
  • dp[1H]=INFdp[1 \dots H] = \text{INF} (求最小值,初始要设为无穷大)。

转移方程 (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) 这个压缩技巧,这正是你想要的!

cpp
copy
        
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. 总结这个套路

以后只要看到题目要求:

  1. “至少” 获得 WW 的重量/体积/分数。
  2. 最小 代价(或者在满足至少 WW 的情况下求最大收益)。

通解模板就是:

  1. 数组开到 WW 大小。
  2. 转移时目标下标写成 min(W, j + w)
  3. dp[W]dp[W] 即为答案。

这道 P2918 是最纯粹的练习题,快去切了它!🔪

代码

[include-code] Error: Failed to read file ./1.cpp.
ENOENT: no such file or directory, open '/home/runner/work/rbook_nunjucks/rbook_nunjucks/problems/luogu/2918/1.cpp'

代码2

另一种风格

这份代码使用了 完全背包 的思路来解决问题,并且使用了一种和我们之前讨论的“状态压缩到 HH”略有不同的策略:“允许越界,最后扫描”

这两种方法都是正确的。这份代码的做法是:既然要凑够至少 HH,那我就干脆算到 H+max_weightH + \text{max\_weight} 这么多,最后在 H\ge H 的范围内找一个最小值。

下面是详细的注释版本:

cpp
copy
        
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; }

💡 代码点评与对比

这份代码和你刚才学的“压缩法”有两个主要区别:

  1. 背包类型

    • P1642 和 P4377 是 01背包(每种只能选一次),所以内层循环是 j--(倒序)。
    • 这份代码(P2918 Buying Hay)是 完全背包(每种可以无限选),所以内层循环是 j++(正序)。
  2. 处理“至少为 H”的方法

    • 方法一(压缩法 - 你之前的理解):

      把所有超过 HH 的都算在 dp[H]dp[H] 里。

      优点:数组开得小,只要开到 HH

      代码特征:min(H, j+w)。

    • 方法二(越界法 - 这份代码):

      把数组开大一点,一直算到 H+MaxWeightH + \text{MaxWeight}。最后在这个范围内找最小值。

      优点:逻辑直观,不需要处理 min 边界。

      代码特征:数组开得大 (m = h + 5000),最后有一个循环 for(i=h; i<=m) 找答案。

这份代码写得非常标准,是解决完全背包“至少”问题的经典写法之一!