0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1616,日期见页头。两边不一致时信原站。
题目背景
此题为纪念 LiYuxiang 而生。
题目描述
LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。 医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说: “孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。 我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子, 你应该可以让采到的草药的总价值最大。”
如果你是 LiYuxiang,你能完成这个任务吗?
此题和原题的不同点:
- 每种草药可以无限制地疯狂采摘。
- 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!
输入格式
输入第一行有两个整数,分别代表总共能够用来采药的时间 t 和代表山洞里的草药的数目 m。
第 2 到第 (m + 1) 行,每行两个整数,第 (i + 1) 行的整数 aᵢ, bᵢ
分别表示采摘第 i 种草药的时间和该草药的价值。
输出格式
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
数据规模与约定
- 对于 30% 的数据,保证
m ≤ 10³。 - 对于 100% 的数据,保证
1 ≤ m ≤ 10⁴,1 ≤ t ≤ 10⁷,且1 ≤ m × t ≤ 10⁷,1 ≤ aᵢ, bᵢ ≤ 10⁴。
输入输出样例
输入
70 3 71 100 69 1 1 2
输出
140
时间 70,三种草药 (71,100) (69,1) (1,2)。第一种根本采不起(71 > 70),
最优是把第三种采 70 次:70 × 2 = 140。
★ 这一组样例当场挡住了「写成倒序」(那一版只能每种采一次,答案 3,差 46 倍), ⚠ 而放过了另外两个错法(32 位溢出、性价比贪心)。
1★ 和 P1048 只差一个字:内层循环的方向
// P1616 疯狂的采药 —— ★ 这一版就能 AC//// 题意:时间 t,m 种草药,第 i 种采一次要 a[i] 时间、值 b[i];**每种可以无限次采**。// 求能采到的最大总价值。//// ★ 它就是[第 23 章 P1048 采药](/sol/p1048/)的完全背包版 ——// 两道题的代码只差**一个字**:内层循环的方向。// 01 背包(每种一件) :for (j = t; j >= a; j--)// 完全背包(每种无限) :for (j = a; j <= t; j++) ← 就是这一行// [第 24 章第 ⑧ 步](/ch/24-knapsack-multi/)把这件事讲透了:// 正序之所以「错」在 01 背包里,正是因为它**恰好在解完全背包**。//// ⚠⚠ 而这道题真正咬人的地方在**数据范围那三行**(解析页第 ② ③ 步):// ① `1 ≤ m × t ≤ 10^7` —— 出题人**直接把复杂度写在题面上了**:`O(mt)` 恒 ≤ 10^7。// (只看 `t ≤ 10^7` 和 `m ≤ 10^4` 会以为是 10^11,那就不敢写这个 DP 了。)// ② 答案会**超过 int**:`t = 10^7`、`a = 1`、`b = 10^4` ⇒ 答案 10^11。// ③ 而 `f` 数组有 `t+1` 格 ⇒ 顶格时 `long long` 要 **76 MB**(限制 128 MB,塞得下)。
#include <bits/stdc++.h>using namespace std;
int main() { int t, m; if (!(cin >> t >> m)) return 0; vector<long long> f(t + 1, 0); // ⚠ long long:答案能到 10^11 for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; for (int j = a; j <= t; j++) // ★ 正序 —— 每种草药可以重复采 f[j] = max(f[j], f[j - a] + b); } cout << f[t] << "\n"; return 0;}点「运行 ▶」看结果
01 背包(每种一件) :for (j = t; j >= a; j--) <- [P1048 采药](/sol/p1048/)
完全背包(每种无限) :for (j = a; j <= t; j++) <- 这道题
第 23 章第 ⑪ 步说过:01 背包的一维写成正序不是随机地错, 它精确地在解完全背包。
⇒ 这两道题(P1048 / P1616)就是那句话的实物:同一份代码、同一份数据, 换一个方向就从一道题变成另一道题。 两题对着交一遍,方向的差别一辈子忘不了。
★ 参照物也跟着换了:01 背包的暴力是「每件拿或不拿」,
而这里是「第 i 种采几株」—— 分支数从 2 变成 t/aᵢ + 1。
2★★★ 数据范围那三行:出题人直接把复杂度写在题面上了
题面的数据范围里有一句很容易滑过去的话:
1 ≤ m ≤ 10⁴,1 ≤ t ≤ 10⁷,且 1 ≤ m × t ≤ 10⁷
DP 是 O(mt)。只看前两个上限,会算成 10⁴ × 10⁷ = 10¹¹ —— 那就根本不敢写这个 DP 了。
只看 m ≤ 10⁴、t ≤ 10⁷ 各自顶格 |
10¹¹ |
加上「m × t ≤ 10⁷」这一句 |
★ 10⁷ |
| 差 | ★ 10 000 倍 |
⇒ 两个上限的乘积被单独限制了一次 —— 这在题面里是罕见的写法,
而它的意思非常直白:「你想写的那个 O(mt) 的 DP,我保证它跑得完」。
★ 这是本书第 N 次「数据范围那几行是工具」(P1080 的两件工具、
P1060 的 m < 25),而这一次工具的形状是乘积。
3★★★ 而题面没说的那一句:答案会溢出,数组还不能随便开大
题面没有给「答案不超过多少」的保证。那就自己算:
每株价值 ≤ 10⁴,采一株最少花 1 单位时间
⇒ 答案 ≤ t × 10⁴ ≤ 10⁷ × 10⁴ = 10¹¹
int 只到 2.1 × 10⁹,差着 47 倍。而溢出的那条线也是算得出来的,还不止一条:
a = 1、b = 10⁴ 时答案 = t × 10⁴ |
越线要 t ≥ |
那时 m 还能到 |
|---|---|---|
越过 int(2³¹−1) |
214 749 | 46 |
越过 unsigned(2³²−1) |
429 497 | 23 |
两条线都在题面允许的范围内(m × t ≤ 10⁷ 都满足)。
造 t = 429497、m = 23(m × t = 9 878 431 ≤ 10⁷ ✓)、a = 1、b = 10⁴:
| 正解 | 4 294 970 000 |
| 32 位版 | ★ 2704 |
| 差 | ★ 4 294 967 296 = 2³²,一分不差 |
★ 而官方样例放过了它(答案才 140)—— 又一次「样例挡住的都是每组都错的」。
f 有 t + 1 格,顶格 t = 10⁷:
long long(8 字节) |
76 MB |
| 内存限制 | 128 MB |
⇒ 塞得下,不用为了省内存去冒溢出的险。 ★ 这一笔和 P2925 正好相反:那道题是「时间够、空间不够」, 这道题是「空间够、类型不够」。⇒ 每道题都要把两个算术各算一遍。
4★★ 错法二:写成倒序 —— 而它的盲区能一句话证出来
// P1616 错法一:内层写成倒序 —— 它精确地解回了 01 背包//// 这是[第 24 章第 ⑧ 步](/ch/24-knapsack-multi/)那句话的反向现场:// **完全背包写成倒序,输出恒等于同一组数据的 01 背包答案。**// 解析页第 ④ 步把这句话量成了「300 组逐组相等」。//// ★ 官方样例当场挡住它:`t = 70`,三种草药 (71,100)(69,1)(1,2)// ⇒ 完全背包能把第三种采 70 次 = 140;而 01 背包每种只能采一次,// 最好是 69 + 1 两株 = 1 + 2 = 3。差了 46 倍。
#include <bits/stdc++.h>using namespace std;
int main() { int t, m; if (!(cin >> t >> m)) return 0; vector<long long> f(t + 1, 0); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; for (int j = t; j >= a; j--) // ⚠ 倒序 —— 每种只采得了一次 f[j] = max(f[j], f[j - a] + b); } cout << f[t] << "\n"; return 0;}点「运行 ▶」看结果
先说清楚它算了什么:
| 随机 300 组:倒序的答案 vs 同一组数据的 01 背包答案 | ★ 300 / 300 逐组相等 |
t ≤ 40 固定,只抬 a 的下限(a ∈ [下限, 40]),各 300 轮:
a 的下限 |
1 | 6 | 14 | 21 |
|---|---|---|---|---|
| 「存在某种草药能采 ≥ 2 株」的轮数 | 145 | 107 | 32 | ★ 0 |
| 倒序真被抓的轮数 | 133 | 89 | 23 | ★ 0 |
⇒ 最后一档那个 0 是结构性的,而且一句话就能证:
a ≥ 21 > t/2 ⇒ 每种草药最多只采得起一株 ⇒ 完全背包和 01 背包本来就是同一道题。
★ 而前三档里「触发条件」比「真被抓」多一点点(145 vs 133、107 vs 89、32 vs 23)—— 能采两株不等于采两株更优。⇒ 又一次:这两个数只能量,不能推。
5★★★ 错法三:性价比贪心 —— 同一个贪心,三道题三种命运
// P1616 错法三:性价比贪心 —— 一直采「单位时间价值最高」的那一种//// 完全背包里这个贪心比在 01 背包里**离对更近**:既然可以无限采,// 那就一直采性价比最高的那种,剩下的零头再拿次高的补 —— 听起来很有道理。//// ⚠ 它仍然是错的,而且错的**方式**和 [P1048](/sol/p1048/) 不一样:// 01 背包里它错在「一件也不能多拿」,这里它错在**凑不满最后那点零头**。// 解析页第 ⑤ 步把三道题的同一个贪心并排量了一遍:// [P2240 部分背包](/sol/p2240/)(能切)—— **它是对的**;// [P1048 01 背包](/sol/p1048/) —— 300 轮错 57 次,错时平均差 10.96%;// 本题(完全背包) —— 见那一页的表。//// ★ 这里如实写成「按性价比排序,从高到低能采就一直采」。
#include <bits/stdc++.h>using namespace std;
int main() { int t, m; if (!(cin >> t >> m)) return 0; vector<pair<int, int>> h(m); // (a 时间, b 价值) for (auto& e : h) cin >> e.first >> e.second;
sort(h.begin(), h.end(), [](const pair<int, int>& x, const pair<int, int>& y) { return (long long)x.second * y.first > (long long)y.second * x.first; // b/a 从大到小 });
long long ans = 0; int left = t; for (auto& e : h) { long long cnt = left / e.first; ans += cnt * e.second; left -= (int)(cnt * e.first); } cout << ans << "\n"; return 0;}点「运行 ▶」看结果
既然可以无限采,那就一直采「单位时间价值最高」的那种,剩下的零头再补 —— 听着比在 01 背包里靠谱多了。
量一遍(m ≤ 5、a ≤ 12、b ≤ 20,只拧 t,各 300 轮):
t 的上限 |
20 | 40 | 200 | 2000 |
|---|---|---|---|---|
| 贪心被抓 | 12 | 12 | 17 | 16 |
| 错时平均差 | 6.74% | 6.77% | 1.21% | ★ 0.18% |
| 错时最差 | 13.33% | 13.33% | 4.10% | 0.64% |
| 题 | 物品能不能拆 | 性价比贪心 | 量出来 |
|---|---|---|---|
| P2240 部分背包 | ★ 能切 | ★ 对的 | 交换论证证得出来 |
| P1048 采药(01 背包) | 每种一件 | 错 | 300 轮错 57 次,错时平均差 10.96% |
| P1616(完全背包) | 每种无限 | 错 | 300 轮错 12~17 次,错时平均差 0.18%~6.77% |
⇒ 「能切」是那条分界线(P1048 那页把交换论证断在哪一步说清楚了: 「换 δ 重量」这个动作在不可拆分的背包里根本不存在)。
★★ 而这一页补上的是中间那一档:完全背包里贪心离对更近,
因为「无限次」让它能把大部分时间填满,只剩最后一点零头凑不上。
⚠⚠ 而这恰恰让它更危险:t 越大它越像对的(2000 那档错时只差 0.18%)——
随手编几组数据一测,你几乎不可能发现它错。
6度量程序和生成器
7一页纸
| 关键的一步 | 和 P1048 只差一个字:内层 for (j = a; j <= t; j++)(正序) |
| 哪一版能 AC | p1616.cpp —— 一维正序,O(mt) |
★★★ 题面那句 m × t ≤ 10⁷ |
出题人直接把复杂度写在题面上了(各自顶格会误算成 10¹¹,差 10 000 倍) |
| ★★★ 题面没说的那句 | 答案上界 10¹¹ ⇒ 必须 long long;int 的线是 t ≥ 214 749、unsigned 是 t ≥ 429 497,都在题面内 |
| 实测溢出 | t=429497, m=23 ⇒ 正解 4 294 970 000,32 位打出 2704(正好差一个 2³²) |
| 内存 | f 顶格开 long long 是 76 MB(限制 128)⇒ 塞得下,不必冒险 |
| 错法一 | 32 位 —— 样例放过 |
| 错法二 | 内层倒序 ⇒ 恒等于 01 背包(300/300 逐组相等);★ 样例当场挡住(3 vs 140) |
| ★ 那个盲区 | a > t/2 时倒序是精确的 0 —— 因为那时每种本来就只采得起一株 |
| 错法三 | 性价比贪心 —— ⚠ t 越大越像对的(2000 那档错时只差 0.18%) |
| ★★ 三道题一条线 | 能切 ⇒ 贪心对(P2240)/01 背包错 10.96%(P1048)/完全背包错 0.18%~6.77% |