题单 · 习题解析

洛谷 P1048 [NOIP 2005 普及组] 采药

★★★ 题单说「这一发 WA 值得挨」—— 量出来它值多少:81% 的轮次性价比贪心是对的,错时平均只差 10.96%;而抓获率是单峰的、「错多少」却是单调的

原题:洛谷 P1048出自 第 20 章 贪心的正确性:交换论证 + 用对拍打假错误贪心 的题单出自 第 23 章 01 背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1048,日期见页头。两边不一致时信原站。

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。 医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说: 「孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。 我会给你一段时间,在这段时间里,你可以采到一些草药。 如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。」

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 个整数 T (1 ≤ T ≤ 1000)M (1 ≤ M ≤ 100),用一个空格隔开, T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。

接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数, 分别表示采摘某株草药的时间和这株草药的价值

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

数据规模与约定

  • 对于 30% 的数据,M ≤ 10
  • 对于全部的数据,M ≤ 100

NOIP 2005 普及组第三题。

输入输出样例

输入

70 3
71 100
69 1
1 2

输出

3

三株草药:(71,100)(69,1)(1,2),总时间 70。 第一株就超时了,只能采后两株 —— 69 + 1 = 70,价值 1 + 2 = 3。 ⚠ 这组样例放过了本页两个贪心错法,只挡住了 DP 那个 —— 第 ⑥ 步会说为什么。

★ 这是第 20 章第 ⑥ 步那个「最著名的上当」的原题

第 20 章第 ⑥ 步讲的就是这道题的模型: 01 背包按性价比贪心。正文已经用对拍把它打假了。

而这一章的题单里,给这道题写了一句更狠的话:

故意先用性价比贪心交一发,看着它 WA,再学第 23 章的 DP。这一发 WA 值得挨。

⇒ 这一页干的就是一件事:把「这一发」值多少分量出来。 而量完会发现,它之所以能骗到这么多人,是有具体数字支撑的。

1正解:01 背包(第 23 章的内容,这里先用)

p1048.cpp★ 这一版就能 AC
// P1048 [NOIP 2005 普及组] 采药 —— ★ 这一版就能 AC(01 背包)
//
// 题意:M 株草药,第 i 株要花 t_i 时间、值 v_i;总时间 T,每株**要么整株采、要么不采**,求最大总价值。
//
// ★ 这就是**01 背包**的模板题,而它挂在[第 20 章](/ch/20-greedy-proof/)的题单里是为了别的事:
// 题单里写着「**故意先用性价比贪心交一发,看着它 WA**,再学第 23 章的 DP。这一发 WA 值得挨」。
// ⇒ 这一页就把那一发**值多少分**量出来(页面第 ③ ④ 步)。
//
// DP:`f[j]` = 时间恰好用不超过 j 时的最大价值。
// `f[j] = max(f[j], f[j - t_i] + v_i)`,**j 必须从大到小枚举**。
// ⚠ 从小到大就变成了**完全背包**(同一株草药能采好几次)——
// 页面第 ⑤ 步把这件事量成了「它逐组等于完全背包的答案」,而**官方样例正好挡住了它**。
//
// 复杂度 `O(T M)`,顶格 1000 × 100 = 10⁵ —— 小得可以忽略。
#include <bits/stdc++.h>
using namespace std;
int main() {
int T, m;
if (!(cin >> T >> m)) return 0;
vector<int> t(m), v(m);
for (int i = 0; i < m; i++) cin >> t[i] >> v[i];
vector<int> f(T + 1, 0);
for (int i = 0; i < m; i++)
for (int j = T; j >= t[i]; j--) // ★ 倒序:每株草药只能用一次
f[j] = max(f[j], f[j - t[i]] + v[i]);
cout << f[T] << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

f[j] = 时间不超过 j 时的最大价值,f[j] = max(f[j], f[j - t] + v)第二层循环必须从大到小。复杂度 O(T M) = 1000 × 100 = 10⁵,小得可以忽略。

2参照物:枚举所有子集

这一页要验的恰恰是「哪个想法对」,所以参照物必须离开「排序贪心」和「DP」两条路:

p1048Brute.cpp参照物:2^M 枚举子集
// P1048 的参照物:**枚举所有子集**(采 / 不采),`2^M`
//
// ★ 它不需要任何想法 —— 而这一页要验的恰恰是「哪个想法是对的」,
// 所以参照物必须离开「排序 + 贪心」和「DP」两条路。
// ⚠ 只跑得动 `M ≤ 22` 左右;题面 30% 的数据是 `M ≤ 10`,那一档正好够。
#include <bits/stdc++.h>
using namespace std;
int main() {
int T, m;
if (!(cin >> T >> m)) return 0;
vector<int> t(m), v(m);
for (int i = 0; i < m; i++) cin >> t[i] >> v[i];
int best = 0;
for (int mask = 0; mask < (1 << m); mask++) {
int st = 0, sv = 0;
for (int i = 0; i < m; i++)
if (mask >> i & 1) { st += t[i]; sv += v[i]; }
if (st <= T) best = max(best, sv);
}
cout << best << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

★ 它跑得动的规模,题面又一次替你写好了:「对于 30% 的数据,M ≤ 10」。

300 组(M ≤ 12):DP vs 枚举所有子集 不一致 0 组

3★★★ 「这一发 WA」值多少分

p1048Ratio.cpp错法一:按性价比贪心
// P1048 错法一:★★★ 按**性价比**(v / t)从高到低贪心 —— 第 20 章那个「最著名的上当」
//
// 直觉非常强:「每分钟能采到的价值最高的先采」。
// 它在**部分背包**(能把草药切开)里是**对的**(见同一天那份 [P2240](/sol/p2240/)),
// 而在这里 —— 草药只能整株采 —— **是错的**。
//
// ★ 交换论证在这里断在哪:部分背包能把 δ 从低性价比换成高性价比,重量不变;
// 01 背包换不了 —— 你只能整株拿或整株不拿,「换 δ」这个动作不存在。
//
// ⚠ 而它错得**不多**(页面第 ③ 步量了):多数随机数据上它只差几个百分点,
// 所以「样例过了、随手试几组也对」是这个上当的常态。
// ★ 比较用交叉相乘(`v1 * t2 > v2 * t1`),全整数,没有浮点。
#include <bits/stdc++.h>
using namespace std;
int main() {
int T, m;
if (!(cin >> T >> m)) return 0;
vector<pair<int, int>> a(m); // (时间, 价值)
for (auto& p : a) cin >> p.first >> p.second;
sort(a.begin(), a.end(), [](const pair<int, int>& x, const pair<int, int>& y) {
return (long long)x.second * y.first > (long long)y.second * x.first; // v1/t1 > v2/t2
});
int cap = T, ans = 0;
for (auto& [t, v] : a)
if (t <= cap) { cap -= t; ans += v; } // 整株采,采不下就跳过
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「每分钟能采到的价值最高的先采」—— 这个直觉强到几乎没人怀疑。量一量它:

随机 300 轮(M ≤ 12T ≤ 200
性价比贪心的轮数 57 / 300(也就是 81% 的轮次它是对的
错的时候,平均比正解少 10.96%
错得最狠的一轮 62.35%
按价值降序贪心错的轮数 50 / 300(错时平均少 17.81%)
★ 这三个数字合起来,就是这个上当能骗到人的全部原因

它不是「经常错得离谱」,而是「偶尔错一点点」。

  • 你随手编三组数据试 —— 八成三组都对;
  • 你交上去 —— WA 在第 4 个测试点,而输出看着完全合理;
  • 你盯着自己的输出和标准答案比 —— 差 10%,第一反应是「哪里少算了一株」, 而不是「整个想法是错的」。

⇒ 这就是第 20 章开头那句话的具体版本: 贪心是唯一一类「代码没写错、样例全过、编译零警告,但整个想法是错的」算法。

4★★★ 而它多久出错,主语不是 M,是「一株草药相对总时间有多大」

顺手写生成器的时候,T 该取多大?这个选择直接决定你能不能抓到这个 bug:

T 的上限(草药 t ≤ 100 不变) 50 200 1 000 5 000
性价比贪心的轮数 / 300 20 61 31 2
错时平均差 12.66% 8.54% 5.77% 2.21%
★★★ 两条曲线的形状不一样 —— 别把它们混成一个数
  • 抓获率是单峰的T 太小,草药常常一株都塞不下、贪心没得选; T 太大,草药相对时间太碎、贪心几乎最优。峰在中间。
  • 「错多少」却是单调下降的T 越大,零头越小,差距越小。

⇒ 两件事要分开问:「多久错一次」和「错的时候差多少」。 ★ 抓获率单峰这件事,第 13 章 P1596 那页量过一次(沿密度单峰), 这一页是它的第二次;而「顺手写的生成器」容易一头扎进 T 很大那一档 —— 那儿 300 轮只抓 2 次

5★★★ 同一个贪心,为什么在 P2240 里就对了 —— 把差值量出来

第 20 章第 ⑨ 步论证过这件事,同一天那份 P2240 部分背包整页都在用这个贪心。 同一个「按性价比排序」,一个对一个错,差别只有一句话:草药能不能切开。

交换论证断在哪一步

部分背包里的论证是:「把已拿的低性价比那部分换掉 δ 重量,换成高性价比的 δ, 重量不变、价值不降」。

01 背包里这个动作不存在 —— 你只能整株拿或整株不拿, 「换 δ」这句话在这里没有意义。论证不是「弱了一点」,是整句话说不出口

把两边的最优值放在一起量(同一批草药,300 组):

能切时的最优(= 性价比贪心,P2240 那道题) 比不能切时平均高 ★ 25.90%
「能切的最优 < 不能切的最优」出现的次数 0(能切只会更好,这是必然的)

⇒ 所以性价比贪心在 01 背包上算出来的那个数,连「能切时的最优」都不是 —— 它是「能切时的最优」被『整株』这个约束砍掉一截之后,再用一个不保证的顺序去凑的结果。

6★★ 另外那个 bug:说清楚它「算了什么」

p1048Fwd.cpp错法三:DP 第二层循环写成正序
// P1048 错法三:DP 的第二层循环写成**从小到大**
//
// 一维 01 背包只有一行需要小心:`for (int j = T; j >= t[i]; j--)`。
// 写成 `for (int j = t[i]; j <= T; j++)` 之后,`f[j - t[i]]` 已经是**这一轮更新过的**了 ——
// 于是同一株草药可以被采好几次。
//
// ★★ 说清楚它**算了什么**比说它「错了」有用:**它精确地在解完全背包**
// (页面第 ⑤ 步把这件事量成了「它逐组等于完全背包的答案」)。
// ★ 而**官方样例正好挡住它**:`1 2` 那株草药能被采 70 次 ⇒ 输出 140,而答案是 3。
#include <bits/stdc++.h>
using namespace std;
int main() {
int T, m;
if (!(cin >> T >> m)) return 0;
vector<int> t(m), v(m);
for (int i = 0; i < m; i++) cin >> t[i] >> v[i];
vector<int> f(T + 1, 0);
for (int i = 0; i < m; i++)
for (int j = t[i]; j <= T; j++) // ← 正序:同一株能采好几次
f[j] = max(f[j], f[j - t[i]] + v[i]);
cout << f[T] << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

一维 01 背包只有一行需要小心:for (int j = T; j >= t[i]; j--)。 写成正序之后,f[j - t[i]] 已经是这一轮更新过的了。

而说清楚它算了什么,比说它「错了」有用得多:

随机 300 轮
正序那版 ≡ 完全背包的答案(标准写法) 300 / 300 逐组相等
它和 01 背包不同的轮数 227 / 300

⇒ 它不是「算错了」,它是精确地在解另一道题(每株草药可以采无限次)。 ★ 这是本书反复用的那个动作(P1332 那页说得最清楚): 先说清楚 bug 算的是什么,再说它错在哪 —— 前者能直接推出后者的所有表现。

四个版本在官方样例上:

版本 样例输出 挡住了吗
正解 3 ——
性价比贪心 3 放过
按价值降序 3 放过
DP 正序(完全背包) 140 挡住
★ 连着第六道题,同一条规律

样例挡住的是「每一组都错」的那个(正序 DP 在几乎任何有小物品的数据上都会爆), 放过的是两个「偶尔才错」的贪心。

P1223 / P2240 / P1094 / P1090 / P1080 都是这样。 ⇒ 官方样例只有一组,它天生是个「一测就死」的过滤器。

7度量程序和生成器

p1048Count.cpp度量程序
// P1048 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p1048Count` 人看的版本
// `./p1048Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 五段:
// ① ★ DP ≡ 枚举所有子集(参照物离开「排序贪心」和「DP」两条路);
// ② ★★★ 第 20 章题单说「先用性价比贪心交一发,看着它 WA,**这一发 WA 值得挨**」——
// 量一量这一发**值多少分**:错的比例,以及错的时候平均差几个百分点;
// ③ ★★★ 它的抓获率由什么决定:**不是 M,是「一株草药相对总时间有多大」**;
// ④ ★★★ 和同一天那份 [P2240] 对照:**同一个贪心,能切就对、不能切就错** ——
// 把「能切」和「不能切」的答案差量出来;
// ⑤ ★★ DP 循环写成正序那个 bug「**算了什么**」:它逐组等于**完全背包**的答案。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
static bool CSV = false;
static void row(const char* key, const vector<ll>& v) {
if (!CSV) return;
printf("%s", key);
for (ll x : v) printf(",%lld", x);
printf("\n");
}
struct In { int T; vector<int> t, v; };
/** 正解:01 背包(倒序) */
static int dp01(const In& in) {
vector<int> f(in.T + 1, 0);
for (size_t i = 0; i < in.t.size(); i++)
for (int j = in.T; j >= in.t[i]; j--) f[j] = max(f[j], f[j - in.t[i]] + in.v[i]);
return f[in.T];
}
/** 错法三:正序 —— 它其实在解完全背包 */
static int dpFwd(const In& in) {
vector<int> f(in.T + 1, 0);
for (size_t i = 0; i < in.t.size(); i++)
for (int j = in.t[i]; j <= in.T; j++) f[j] = max(f[j], f[j - in.t[i]] + in.v[i]);
return f[in.T];
}
/** 完全背包的标准写法(拿来和上面那个 bug 逐组比) */
static int dpComplete(const In& in) {
vector<int> f(in.T + 1, 0);
for (int j = 1; j <= in.T; j++)
for (size_t i = 0; i < in.t.size(); i++)
if (in.t[i] <= j) f[j] = max(f[j], f[j - in.t[i]] + in.v[i]);
return f[in.T];
}
/** 错法一:性价比贪心(整株拿) */
static int greedyRatio(const In& in) {
int m = in.t.size();
vector<int> idx(m);
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](int x, int y) {
return (ll)in.v[x] * in.t[y] > (ll)in.v[y] * in.t[x];
});
int cap = in.T, ans = 0;
for (int i : idx) if (in.t[i] <= cap) { cap -= in.t[i]; ans += in.v[i]; }
return ans;
}
/** 错法二:按价值降序 */
static int greedyByV(const In& in) {
int m = in.t.size();
vector<int> idx(m);
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](int x, int y) { return in.v[x] > in.v[y]; });
int cap = in.T, ans = 0;
for (int i : idx) if (in.t[i] <= cap) { cap -= in.t[i]; ans += in.v[i]; }
return ans;
}
/** ★ 同一批草药,如果**能切**(部分背包):性价比贪心就是最优解。返回 ×100 的整数值 */
static ll fracBest100(const In& in) {
int m = in.t.size();
vector<int> idx(m);
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](int x, int y) {
return (ll)in.v[x] * in.t[y] > (ll)in.v[y] * in.t[x];
});
ll cap = in.T, ans100 = 0;
for (int i : idx) {
if (in.t[i] <= cap) { cap -= in.t[i]; ans100 += 100LL * in.v[i]; }
else { ans100 += 100LL * in.v[i] * cap / in.t[i]; cap = 0; break; }
}
return ans100;
}
/** 参照物:枚举所有子集 */
static int brute(const In& in) {
int m = in.t.size(), best = 0;
for (int mask = 0; mask < (1 << m); mask++) {
int st = 0, sv = 0;
for (int i = 0; i < m; i++) if (mask >> i & 1) { st += in.t[i]; sv += in.v[i]; }
if (st <= in.T) best = max(best, sv);
}
return best;
}
static In gen(mt19937& rng, int mHi, int tHi, int vHi) {
In in;
int m = (int)(rng() % (unsigned)mHi) + 1;
in.T = (int)(rng() % (unsigned)tHi) + 1;
in.t.resize(m); in.v.resize(m);
for (int i = 0; i < m; i++) { in.t[i] = (int)(rng() % (unsigned)vHi) + 1; in.v[i] = (int)(rng() % (unsigned)vHi) + 1; }
return in;
}
int main(int argc, char** argv) {
CSV = argc > 1 && string(argv[1]) == "csv";
/* ① DP ≡ 枚举所有子集 */
{
mt19937 rng(20260829u);
int groups = 0, bad = 0;
for (int rep = 0; rep < 300; rep++, groups++) {
In in = gen(rng, 12, 200, 100);
if (dp01(in) != brute(in)) bad++;
}
if (!CSV) printf("① DP vs 枚举所有子集:%d 组(M ≤ 12),不一致 %d 组\n", groups, bad);
row("ref", {groups, bad});
}
/* ② 「这一发 WA」值多少分 */
{
mt19937 rng(1048u);
int rounds = 300, badR = 0, badV = 0, badF = 0;
long double lossR = 0, lossV = 0, worstR = 0;
for (int r = 0; r < rounds; r++) {
In in = gen(rng, 12, 200, 100);
int ok = dp01(in);
int gr = greedyRatio(in), gv = greedyByV(in);
if (gr != ok) { badR++; long double d = ok ? (long double)(ok - gr) / ok : 0; lossR += d; worstR = max(worstR, d); }
if (gv != ok) { badV++; lossV += ok ? (long double)(ok - gv) / ok : 0; }
if (dpFwd(in) != ok) badF++;
}
ll avgR = (ll)(badR ? lossR / badR * 10000 + 0.5L : 0); // 万分之几
ll avgV = (ll)(badV ? lossV / badV * 10000 + 0.5L : 0);
ll wR = (ll)(worstR * 10000 + 0.5L);
if (!CSV) printf("② %d 轮:性价比贪心错 %d 次(错时平均只差 %.2f%%,最差 %.2f%%);"
"按价值排错 %d 次(平均差 %.2f%%);DP 正序错 %d 次\n",
rounds, badR, avgR / 100.0, wR / 100.0, badV, avgV / 100.0, badF);
row("wa", {badR, avgR, wR, badV, avgV, badF});
}
/* ③ 抓获率的主语:一株草药相对总时间有多大 */
{
const int TS[] = {50, 200, 1000, 5000}; // T 的上限;草药 t ≤ 100 不变
vector<ll> out;
for (int tHi : TS) {
mt19937 rng(tHi * 7919u + 5u);
int bad = 0;
long double loss = 0;
for (int r = 0; r < 300; r++) {
In in = gen(rng, 12, tHi, 100);
int ok = dp01(in), gr = greedyRatio(in);
if (gr != ok) { bad++; loss += ok ? (long double)(ok - gr) / ok : 0; }
}
ll avg = (ll)(bad ? loss / bad * 10000 + 0.5L : 0);
out.push_back(bad); out.push_back(avg);
if (!CSV) printf("③ T ≤ %5d(草药 t ≤ 100):性价比贪心错 %d / 300,错时平均差 %.2f%%\n",
tHi, bad, avg / 100.0);
}
row("ratioT", out);
}
/* ④ ★★★ 同一个贪心:能切就对,不能切就错 */
{
mt19937 rng(2240u);
int rounds = 300, fracBad = 0;
long double gap = 0;
for (int r = 0; r < rounds; r++) {
In in = gen(rng, 12, 200, 100);
ll fr100 = fracBest100(in); // 能切时的最优(= 性价比贪心)
ll ok100 = 100LL * dp01(in); // 不能切时的最优
if (fr100 < ok100) fracBad++; // 能切反而更差?不可能
gap += ok100 ? (long double)(fr100 - ok100) / ok100 : 0;
}
ll avgGap = (ll)(gap / rounds * 10000 + 0.5L);
if (!CSV) printf("④ 同一批草药:**能切**时的最优比**不能切**时平均高 %.2f%%;"
"「能切的最优 < 不能切的最优」出现 %d 次(应当是 0)\n", avgGap / 100.0, fracBad);
row("frac", {avgGap, fracBad});
}
/* ⑤ 正序那个 bug 算了什么 */
{
mt19937 rng(999u);
int rounds = 300, same = 0, diffFrom01 = 0;
for (int r = 0; r < rounds; r++) {
In in = gen(rng, 12, 200, 100);
if (dpFwd(in) == dpComplete(in)) same++;
if (dpFwd(in) != dp01(in)) diffFrom01++;
}
if (!CSV) printf("⑤ %d 轮:DP 正序那版 ≡ 完全背包的答案 %d 轮(逐组相等);"
"而它和 01 背包不同 %d 轮\n", rounds, same, diffFrom01);
row("fwd", {rounds, same, diffFrom01});
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1048Gen.cpp数据生成器
// P1048 对拍生成器:`./p1048Gen <seed> [M 上限] [T 上限] [t、v 的上限]`
// 默认 `M ≤ 12`、`T ≤ 200`、`t, v ≤ 100` —— M 压在 12 是为了让 `2^M` 暴力当得了参照物。
//
// ★ 这道题真正该拧的旋钮不是 M,是 **T 相对 t 的比值**(页面第 ④ 步那张表):
// 一株草药相对总时间越小,性价比贪心越接近最优(因为「装不下的零头」越小);
// 草药大到一两株就占满时间,它就开始大幅出错。
// ⇒ 这是[第 7 章 P1102](/sol/p1102/) 那条「不是『小数据』,是**比值**」的又一次。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int mHi = argc > 2 ? atoi(argv[2]) : 12;
int tHi = argc > 3 ? atoi(argv[3]) : 200;
int vHi = argc > 4 ? atoi(argv[4]) : 100;
mHi = max(1, min(100, mHi));
tHi = max(1, min(1000, tHi));
vHi = max(1, min(100, vHi));
mt19937 rng(seed * 2654435761u + 48u);
int m = (int)(rng() % (unsigned)mHi) + 1;
int T = (int)(rng() % (unsigned)tHi) + 1;
printf("%d %d\n", T, m);
for (int i = 0; i < m; i++)
printf("%u %u\n", (unsigned)(rng() % (unsigned)vHi) + 1, (unsigned)(rng() % (unsigned)vHi) + 1);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1048ByV.cpp错法二:按价值降序

8一页纸

关键的一步 01 背包(第 23 章):f[j] = max(f[j], f[j-t] + v),第二层倒序
哪一版能 AC p1048.cppO(T M) = 10⁵
★★★ 这一页的主线 题单说「这一发 WA 值得挨」—— 量出来它值多少
81% 的轮次它是对的,错的时候平均只差 10.96% ⇒ 这就是它骗人的原因
★★★ 抓获率的主语 不是 M,是 T 相对草药 t 的比值;而且它是单峰的(20 / 61 / 31 / 2)——
⚠ 而「错多少」是单调下降的(12.66% → 2.21%):两条曲线形状不同
★★★ 和 P2240 的对照 同一个贪心,能切就对、不能切就错;能切时的最优平均高 25.90%
(交换论证断在「换 δ」这个动作在 01 背包里根本不存在)
★★ 另一个 bug DP 写成正序 ⇒ 它精确地在解完全背包(300 / 300 逐组相等)
参照物 2^M 枚举子集(题面 30% 那一档就够)—— 300 组不一致 0 组
样例的表现 只挡住「每组都错」的那个,两个贪心全放过 —— 连着第六道题同一条规律