题单 · 习题解析

洛谷 P1094 [NOIP 2007 普及组] 纪念品分组

★★★ 「每组最多两件」这五个字是命门 —— 有它这题是最大匹配(好几种贪心都对),把上限改成 3 同一份代码当场就错;⚠ 草稿把 best-fit 当错法,被 11 万组穷举打回来了

原题:洛谷 P1094出自 第 19 章 贪心基础:排序型贪心 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

NOIP2007 普及组 T2

题目描述

元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作。为使得参加晚会的同学所获得的纪念品价值相对均衡, 他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品, 并且每组纪念品的价格之和不能超过一个给定的整数。为了保证在尽量短的时间内发完所有纪念品, 乐乐希望分组的数目最少

你的任务是写一个程序,找出所有分组方案中分组数最少的一种,输出最少的分组数目。

输入格式

n+2 行:

第一行包括一个整数 w,为每组纪念品价格之和的上限。

第二行为一个整数 n,表示购来的纪念品的总件数。

3 ~ n+2 行每行包含一个正整数 Pᵢ 表示所对应纪念品的价格。

输出格式

一个整数,即最少的分组数目。

数据规模与约定

50% 的数据满足:1 ≤ n ≤ 15

100% 的数据满足:1 ≤ n ≤ 3 × 10⁴80 ≤ w ≤ 2005 ≤ Pᵢ ≤ w

输入输出样例

输入

100
9
90
20
20
30
50
60
70
80
90

输出

6

⚠ 注意输入顺序:第一行是 w(价格上限),第二行才是 n —— 比常见的「先 n 后 w」反过来。

★ 这一页的主线:一句约束值多少钱

这道题看着是道普通的排序贪心,但它藏着一件很值钱的事:

题面里「每组最多只能包括两件纪念品」这十几个字,是这道题唯一的命门。 有它,这题是 O(n log n),而且好几种贪心都对; 去掉它,这就是装箱问题 —— NP 难。

⚠ 而这一页有一段是被实测打回来重写的:草稿里我把「每组尽量装满」当成错法, 理由听起来很顺(那是装箱问题里的近似启发式)。穷举 11 万组,一个反例都没有。 第 ④ ⑤ 步就是这件事。

1正解:排序 + 对撞双指针

p1094.cpp★ 这一版就能 AC
// P1094 [NOIP 2007 普及组] 纪念品分组 —— ★ 这一版就能 AC
//
// 题意:n 件纪念品,价格 P_i;**每组最多两件**,每组价格之和 ≤ w,求最少分几组。
//
// ★ 关键一步:排序之后用[第 7 章的对撞双指针](/ch/07-two-pointers/),
// **最贵的那件去配最便宜的那件**:配得上就成一组,配不上就让它自己单独一组。
//
// 交换论证(比排队接水那次绕一点):设最贵的是 X。
// · 如果最便宜的 Y 都配不上 X(`X + Y > w`),那**谁都配不上 X** ⇒ X 只能单独一组;
// · 如果配得上,那么让 X 配 Y **不吃亏**:假设最优解里 X 配的是 Z、Y 配的是 U,
// 把它们换成 (X, Y) 和 (Z, U) —— `Z + U ≤ X + Z ≤ w`(因为 Y 最小、X 最大),
// ⇒ 换完仍然合法,组数一个没多。
// ⇒ 所以「最贵配最便宜」一定能达到最优。
//
// ⚠ 三处容易栽的地方(页面上各量了一个):
// ① 输入格式是 **w 在第一行、n 在第二行**(比常见的「先 n 后 w」反过来);
// ② 循环条件是 `l <= r` 不是 `l < r` —— 剩下最后一件时它也要自己成一组;
// ③ ★★★ 一个**看起来更聪明**的贪心(每组尽量填满 w,也就是装箱里的 best-fit)
// 在这道题上是**错的**,而它错得很轻 —— 页面第 ④ 步量了它。
//
// 复杂度 O(n log n),全花在排序上(★ 而 P_i ≤ 200,桶排能做到 O(n + w))。
#include <bits/stdc++.h>
using namespace std;
int main() {
int w, n;
if (!(cin >> w >> n)) return 0; // ★ 先 w 后 n
vector<int> a(n);
for (int& x : a) cin >> x;
sort(a.begin(), a.end());
int l = 0, r = n - 1, cnt = 0;
while (l <= r) { // ★ 是 <=
if (a[l] + a[r] <= w) l++; // 最便宜的能搭上最贵的,一起走
r--; // 最贵的这件无论如何都走
cnt++;
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
交换论证:为什么「最贵的配最便宜的」

设剩下的里面最贵的是 X、最便宜的是 Y

  • 如果 X + Y > w,那么谁都配不上 XY 已经是最小的了)⇒ X 只能自己一组;
  • 如果 X + Y ≤ w,那么让 XY 不吃亏: 假设某个最优解里 X 配的是 ZY 配的是 U, 把它们换成 (X, Y)(Z, U) —— 因为 Y 最小、X 最大,所以 Z + U ≤ X + Z ≤ w换完仍然合法,组数一个没多

⇒ 「最贵配最便宜」一定能达到最优。这就是第 7 章的对撞双指针

2参照物:枚举所有分组方案

这一页的三个对照写法都只差一两行,而且每一个听起来都有道理。 所以参照物必须彻底离开贪心这条路:

p1094Brute.cpp参照物:枚举所有分组方案
// P1094 的参照物:**枚举所有分组方案**(不排序、不贪心)
//
// ★ 这道题的三个错法都只差一两行,而且每一个都「听起来有道理」——
// 所以参照物必须彻底离开贪心这条路。
//
// 造法:递归。取还没分组的第一件,它要么**自己单独一组**,
// 要么和后面某一件配成一组(前提是和 ≤ w)。取所有分支里组数最少的。
// 复杂度大约 O(n!!),只跑得动 n ≤ 12 —— 但它**不需要任何想法**,这正是它的价值。
#include <bits/stdc++.h>
using namespace std;
static int n, w, best;
static vector<int> a;
static vector<char> used;
static void dfs(int done, int groups) {
if (groups >= best) return; // 已经不可能更好
if (done == n) { best = min(best, groups); return; }
int i = 0;
while (i < n && used[i]) i++; // 第一件还没分组的
used[i] = 1;
dfs(done + 1, groups + 1); // ① 它自己一组
for (int j = i + 1; j < n; j++) { // ② 和后面某一件配
if (used[j] || a[i] + a[j] > w) continue;
used[j] = 1;
dfs(done + 2, groups + 1);
used[j] = 0;
}
used[i] = 0;
}
int main() {
if (!(cin >> w >> n)) return 0;
a.assign(n, 0);
for (int& x : a) cin >> x;
used.assign(n, 0);
best = n + 1;
dfs(0, 0);
cout << best << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

取还没分组的第一件,它要么自己一组、要么和后面某一件配。递归到底取最小。 跑得动 n ≤ 12,但它不需要任何想法 —— 这正是它的价值。

双指针 vs 枚举所有分组方案(n ≤ 10,值域照题面) 400 组,不一致 0 组

3两个真错法,以及又一个「一个不差」

p1094Lt.cpp错法一:循环写成 l < r
// P1094 错法一:循环条件写成 `l < r`
//
// 剩下最后一件(`l == r`)时,它也**必须自己占一组** —— 而 `l < r` 直接把它漏掉了。
//
// ★ 它什么时候现形?**只在「最后剩奇数个」的时候** —— 页面第 ④ 步量了它的抓获率。
#include <bits/stdc++.h>
using namespace std;
int main() {
int w, n;
if (!(cin >> w >> n)) return 0;
vector<int> a(n);
for (int& x : a) cin >> x;
sort(a.begin(), a.end());
int l = 0, r = n - 1, cnt = 0;
while (l < r) { // ← 这里
if (a[l] + a[r] <= w) l++;
r--;
cnt++;
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

剩下最后一件(l == r)时它也必须自己占一组,而 l < r 把它整个漏掉了。

p1094Pair.cpp错法二:排完序相邻两两配
// P1094 错法二:排完序**相邻两两配**
//
// 「排好序,一二一起、三四一起」是很常见的第一反应 ——
// 它把「最贵的配最便宜的」听成了「差不多贵的配在一起」。
//
// 结果正好反过来:两件贵的凑一起往往超过 w,于是两件都得单独成组,
// 而两件便宜的凑一起白白浪费了一个名额。
#include <bits/stdc++.h>
using namespace std;
int main() {
int w, n;
if (!(cin >> w >> n)) return 0;
vector<int> a(n);
for (int& x : a) cin >> x;
sort(a.begin(), a.end());
int cnt = 0;
for (int i = 0; i < n; ) {
if (i + 1 < n && a[i] + a[i + 1] <= w) { i += 2; } // ← 相邻两个配
else { i += 1; }
cnt++;
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「排好序,一二一起、三四一起」—— 把「最贵的配最便宜的」听成了「差不多贵的配一起」。 结果正好反过来:两件贵的凑不上,两件便宜的又白白浪费一个名额。

照题面随机 300 轮(n ≤ 10):

写法 官方样例 被抓的轮数
l < r 5(正解 6)挡住 191
相邻两两配 7(正解 6)挡住 134
★ 又一个「触发条件 ≡ 抓获数」

l < r 那个错法的触发条件是「最后停在 l == r」(也就是最后剩下奇数件要单独成组)。

    最后停在 l == r 的轮数 :  191
    l < r 被抓的轮数       :  191

一个不差 —— 和同一天那份 P2240(「除不尽 239 ≡ 被抓 239」)是同一种结论: 它比「错了 191 次」强得多,因为它等于证明了「只要剩奇数件,就一定错」。

★ 而同一天的另外两道题上,这两个数差 1.9 倍(P1223)和 60 倍P1803)。 ⇒ 这四页凑在一起说的是同一句话:这两个数的关系只能量,不能推。

4★★★ 而第三个「错法」不是错法 —— 这一段是被实测改写的

草稿里还有第三个错法,理由听起来非常顺:

既然每组的和不能超过 w,那当然要让每组尽量装满 —— 从最贵的开始,给它找一件「加起来最接近 w 且不超过 w」的搭档。

这就是装箱问题里的 best-fit(最佳适应),一个经典的近似启发式。 既然是近似,那它当然不是最优解 —— 草稿就是这么写的。

p1094Fit.cpp第二种贪心:每组尽量装满
// P1094 的第二种贪心:★★★ 「每组尽量装满」(best-fit)—— 它**也是对的**
//
// ⚠⚠ 这份文件的注释被实测改写过一次,值得留个记录:
// 草稿里它是「错法三」,理由听起来很顺 —— best-fit(最佳适应)是**装箱问题**里的
// 经典近似启发式,而近似启发式当然不是最优解。
// ⇒ **实测把这个说法打回来了。**
// 穷举 `n = 2..6`、`w = 4..14` 的**全部非降价格序列**(116 050 组):**一个反例都没有**;
// 照题面随机、各种规模各种值域,也全是 0(页面第 ④ 步那几张表)。
//
// ★★★ 为什么它在这道题上是对的,而在装箱问题上不是 —— 差别就是题面那五个字:
// **「每组最多两件」**。
// 一组只能放两件的时候,问题其实是「在一张阈值图上求最大匹配」,
// 而这种图上「最贵的配最便宜的」和「最贵的配最大能配的」**都能达到最大匹配**。
// 一旦允许每组三件,同一份 best-fit 当场就错(页面第 ⑤ 步实测:0 → 5 / 7 / 4)。
//
// ⇒ 所以这份文件真正的用处,是把「每组最多两件」这句约束**称出重量**:
// 它不是背景描述,是**命门** —— 有它,好几种贪心都对;没它,这题是 NP 难的装箱问题。
//
// 复杂度 O(n²),比正解慢,只用来做对照。
#include <bits/stdc++.h>
using namespace std;
int main() {
int w, n;
if (!(cin >> w >> n)) return 0;
vector<int> a(n);
for (int& x : a) cin >> x;
sort(a.begin(), a.end());
vector<char> used(n, 0);
int cnt = 0;
for (int i = n - 1; i >= 0; i--) { // 从最贵的开始
if (used[i]) continue;
used[i] = 1;
int best = -1;
for (int j = 0; j < i; j++) { // 找「加起来最接近 w」的搭档
if (used[j]) continue;
if (a[i] + a[j] > w) break; // a 升序,再往后只会更大
if (best < 0 || a[j] > a[best]) best = j;
}
if (best >= 0) used[best] = 1;
cnt++;
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

实测把这个说法打回来了。

怎么找的反例 结果
穷举 n = 2..6w = 4..14全部非降价格序列(116 050 组) 一个反例都没有
照题面随机,w = 100Pᵢ ∈ [5, 100]n = 4 / 10 / 30 / 100 各 300 轮 0 / 0 / 0 / 0
⚠⚠ 「搜不到反例」之前,先证明搜索本身是活的

「一个反例都没有」和「这段搜索根本没在搜」,从输出上看一模一样 —— 同一天的 P2240 刚为一个「精确的 0」补过同样的自检。

所以这里也补了一道:拿同一段搜索去搜「相邻两两配」那个已知错法

    n = 4,w = 4,价格 = 1 1 2 3      <- 立刻就搜到了
    相邻两两配: 3 组     最优: 2 组

⇒ 搜索是活的。那么上面那个「找不到」就是真的。

5★★★ 那它凭什么对 —— 凭题面那五个字

best-fit 在真正的装箱问题上确实只是个近似。它在这道题上却是对的, 差别只有一处:「每组最多只能包括两件」

★ 一组只放两件的时候,这题根本不是装箱

一组两件 ⇒ 每一组要么是一件单品,要么是一对。 于是「分组最少」等价于「配成的对最多」,也就是一张图上的最大匹配问题 (把 ij 连边当且仅当 Pᵢ + Pⱼ ≤ w)。

而这张图很特别:两点连边只取决于两个价格的和 —— 这样的图叫阈值图。 在阈值图上,「最贵的配最便宜的」和「最贵的配最大能配的」都能达到最大匹配。 ⇒ 两种贪心都对,不是巧合。

而一旦允许每组三件,「配对」这个结构就没了 —— 它立刻退回装箱问题。

把这句话称一称重量。 同一份 best-fit,同一批数据,只把「每组最多几件」从 2 改成 3:

wn = 6..8,每档 300 组) 8 12 20
每组最多 2 件时错的组数 0 0 0
放宽到最多 3 件时错的组数 5 7 4
★★★ 这就是「命门」的判据本身

第 12 章 P1226 那一页把题面里的约束分成三类 —— 情报 / 命门 / 噪声, 判据是「造一档违反它的数据,看有没有任何一版的行为变了」。

这道题上的用法更狠一点:违反它之后,塌掉的不是数据,是算法本身。 0 / 0 / 0 一步跨到 5 / 7 / 4,而代码一个字没改。

⇒ 读题时看到「每组最多两件」「每人最多买一件」「至多选 k 个」这类字样, 先问一句:把这个上限加一,我的算法还成立吗? 多半是不成立的 —— 那说明它是命门,不是背景描述。

6⚠ 一个和算法无关、但每年都有人栽的地方

这道题的输入格式是:

    第 1 行 :  w      <- 价格上限(不是件数!)
    第 2 行 :  n      <- 件数
    第 3 行起:  P_1 .. P_n,每行一个

w 在前、n 在后 —— 而绝大多数题是「先 n 后别的」。读反了会怎样? 样例里 w = 100n = 9:读反就变成「有 100 件纪念品,上限是 9」, 后面 91 个数根本读不到 —— 程序会拿着一堆未初始化的值算下去。

这一条样例挡得住(这也是官方样例的价值之一),但它值得单独说一句: 题面的输入格式要一行一行对着抄,别照着「印象里的样子」写。

7度量程序和生成器

p1094Count.cpp度量程序
// P1094 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p1094Count` 人看的版本
// `./p1094Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 五段:
// ① ★ 双指针 ≡ 枚举所有分组方案(小数据,参照物完全离开贪心这条路);
// ② ★ 三个错法在**照题面随机**上各被抓多少;
// ③ ★★★ best-fit 那个「看起来更聪明」的贪心的**最小反例**(穷举搜出来的);
// ④ ★★★ 它的抓获率由什么决定 —— 拧「P_i 的下限相对 w」这个旋钮;
// ⑤ ★ 「循环写成 l < r」那个错法的触发条件:数一数「最后停在 l == r」的轮数,
// 看它是不是就等于被抓的轮数。
#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");
}
/** 正解:排序 + 对撞双指针。touch 记录「最后是不是停在 l == r」 */
static int solve(vector<int> a, int w, bool* touch = nullptr) {
sort(a.begin(), a.end());
int l = 0, r = (int)a.size() - 1, cnt = 0;
if (touch) *touch = false;
while (l <= r) {
if (l == r && touch) *touch = true;
if (a[l] + a[r] <= w) l++;
r--;
cnt++;
}
return cnt;
}
/** 错法一:l < r */
static int solveLt(vector<int> a, int w) {
sort(a.begin(), a.end());
int l = 0, r = (int)a.size() - 1, cnt = 0;
while (l < r) { if (a[l] + a[r] <= w) l++; r--; cnt++; }
return cnt;
}
/** 错法二:相邻两两配 */
static int solvePair(vector<int> a, int w) {
sort(a.begin(), a.end());
int n = a.size(), cnt = 0;
for (int i = 0; i < n; ) { if (i + 1 < n && a[i] + a[i + 1] <= w) i += 2; else i += 1; cnt++; }
return cnt;
}
/** 错法三:best-fit —— 每组尽量装满 */
static int solveFit(vector<int> a, int w) {
sort(a.begin(), a.end());
int n = a.size(), cnt = 0;
vector<char> used(n, 0);
for (int i = n - 1; i >= 0; i--) {
if (used[i]) continue;
used[i] = 1;
int best = -1;
for (int j = 0; j < i; j++) {
if (used[j]) continue;
if (a[i] + a[j] > w) break;
if (best < 0 || a[j] > a[best]) best = j;
}
if (best >= 0) used[best] = 1;
cnt++;
}
return cnt;
}
/** 参照物:枚举所有分组方案 */
static int gN, gW, gBest;
static vector<int> gA;
static vector<char> gUsed;
static void dfs(int done, int groups) {
if (groups >= gBest) return;
if (done == gN) { gBest = min(gBest, groups); return; }
int i = 0;
while (i < gN && gUsed[i]) i++;
gUsed[i] = 1;
dfs(done + 1, groups + 1);
for (int j = i + 1; j < gN; j++) {
if (gUsed[j] || gA[i] + gA[j] > gW) continue;
gUsed[j] = 1;
dfs(done + 2, groups + 1);
gUsed[j] = 0;
}
gUsed[i] = 0;
}
static int brute(const vector<int>& a, int w) {
gA = a; gN = a.size(); gW = w;
gUsed.assign(gN, 0);
gBest = gN + 1;
dfs(0, 0);
return gBest;
}
/** 照题面造一组:w ∈ [wLo, wHi],P_i ∈ [pLo, w] */
static vector<int> gen(mt19937& rng, int n, int w, int pLo) {
vector<int> a(n);
for (int i = 0; i < n; i++) a[i] = pLo + (int)(rng() % (unsigned)(w - pLo + 1));
return a;
}
int main(int argc, char** argv) {
CSV = argc > 1 && string(argv[1]) == "csv";
/* ① 双指针 ≡ 枚举所有分组方案 */
{
mt19937 rng(20260829u);
int groups = 0, bad = 0;
for (int rep = 0; rep < 400; rep++, groups++) {
int w = 80 + (int)(rng() % 121);
int n = (int)(rng() % 10) + 1;
vector<int> a = gen(rng, n, w, 5);
if (solve(a, w) != brute(a, w)) bad++;
}
if (!CSV) printf("① 双指针 vs 枚举所有分组方案:%d 组(n ≤ 10,值域照题面),不一致 %d 组\n", groups, bad);
row("ref", {groups, bad});
}
/* ② 三个错法在照题面随机上的抓获率 */
{
mt19937 rng(555u);
int lt = 0, pr = 0, ft = 0, touched = 0, ltTouch = 0;
for (int r = 0; r < 300; r++) {
int w = 80 + (int)(rng() % 121);
int n = (int)(rng() % 10) + 1;
vector<int> a = gen(rng, n, w, 5);
bool touch; int ok = solve(a, w, &touch);
if (touch) touched++;
bool caught = solveLt(a, w) != ok;
if (caught) { lt++; if (touch) ltTouch++; }
if (solvePair(a, w) != ok) pr++;
if (solveFit(a, w) != ok) ft++;
}
if (!CSV) printf("② 照题面随机 300 轮(n ≤ 10):l<r 错 %d 次、相邻配对错 %d 次、best-fit 错 %d 次\n"
" (「最后停在 l == r」的有 %d 轮,其中 l<r 被抓 %d 轮)\n",
lt, pr, ft, touched, ltTouch);
row("catch", {lt, pr, ft, touched, ltTouch});
}
/* ③ ★★★ 找 best-fit 的反例 —— 结果是**找不到**,所以先证明这个搜索是活的 */
{
// 穷举 n = 2..6、w = 4..14 的**全部非降价格序列**,看有没有哪一组让某个写法错
auto search = [&](int kind, int& fn, int& fw, vector<int>& fa) {
fn = -1; fw = -1; fa.clear();
for (int n = 2; n <= 6 && fn < 0; n++)
for (int w = 4; w <= 14 && fn < 0; w++) {
vector<int> a(n, 1);
function<void(int, int)> rec = [&](int idx, int lo) {
if (fn >= 0) return;
if (idx == n) {
int got = kind == 0 ? solveFit(a, w) : solvePair(a, w);
if (got != brute(a, w)) { fn = n; fw = w; fa = a; }
return;
}
for (int v = lo; v <= w && fn < 0; v++) { a[idx] = v; rec(idx + 1, v); }
};
rec(0, 1);
}
};
// 顺带数一数这个穷举一共看了多少组
ll seqs = 0;
for (int n = 2; n <= 6; n++)
for (int w = 4; w <= 14; w++) {
// 非降序列个数 = C(w + n - 1, n)
long double c = 1;
for (int i = 1; i <= n; i++) c = c * (w + n - i) / i;
seqs += (ll)(c + 0.5L);
}
int fn, fw; vector<int> fa;
search(0, fn, fw, fa);
int fitFound = fn;
int pn, pw; vector<int> pa;
search(1, pn, pw, pa);
if (!CSV) {
printf("③ 穷举 n = 2..6、w = 4..14 的全部非降价格序列(共 %lld 组):\n", seqs);
printf(" · best-fit(每组尽量装满):**一个反例都没有**(%s)\n",
fitFound < 0 ? "找不到" : "居然找到了");
printf(" ⚠ 自检(证明这个搜索是活的):同一个搜索对「相邻两两配」立刻找到反例 —— "
"n = %d,w = %d,价格 =", pn, pw);
for (int x : pa) printf(" %d", x);
printf("(它 %d 组,最优 %d 组)\n", solvePair(pa, pw), brute(pa, pw));
}
vector<ll> out = {seqs, fitFound < 0 ? 0 : 1, pn, pw};
for (int x : pa) out.push_back(x);
out.push_back(solvePair(pa, pw));
out.push_back(brute(pa, pw));
row("search", out);
}
/* ④ ★★★ 「每组最多两件」是命门:把它放宽到三件,同一个 best-fit 当场就错 */
{
// 最优解(每组最多 K 件):枚举所有分组方案
function<int(const vector<int>&, int, int)> bestK =
[&](const vector<int>& a, int w, int K) {
int n = a.size(), best = n + 1;
vector<char> used(n, 0);
function<void(int, int)> rec = [&](int done, int groups) {
if (groups >= best) return;
if (done == n) { best = min(best, groups); return; }
int i = 0;
while (i < n && used[i]) i++;
// 这一组以 i 打头,再从 i 之后挑最多 K-1 个,总和 ≤ w
vector<int> cand;
for (int j = i + 1; j < n; j++) if (!used[j]) cand.push_back(j);
int m = cand.size();
used[i] = 1;
for (int mask = 0; mask < (1 << m); mask++) {
if (__builtin_popcount(mask) > K - 1) continue;
int sum = a[i], k = 0;
for (int t = 0; t < m; t++) if (mask >> t & 1) { sum += a[cand[t]]; k++; }
if (sum > w) continue;
for (int t = 0; t < m; t++) if (mask >> t & 1) used[cand[t]] = 1;
rec(done + 1 + k, groups + 1);
for (int t = 0; t < m; t++) if (mask >> t & 1) used[cand[t]] = 0;
}
used[i] = 0;
};
rec(0, 0);
return best;
};
// best-fit 的「每组最多 K 件」版本:从最贵的开始,反复塞进当前能塞的最大的一件
auto fitK = [&](vector<int> a, int w, int K) {
sort(a.begin(), a.end());
int n = a.size(), cnt = 0;
vector<char> used(n, 0);
for (int i = n - 1; i >= 0; i--) {
if (used[i]) continue;
used[i] = 1;
int sum = a[i];
for (int k = 1; k < K; k++) {
int best = -1;
for (int j = 0; j < n; j++) {
if (used[j]) continue;
if (sum + a[j] > w) break;
if (best < 0 || a[j] > a[best]) best = j;
}
if (best < 0) break;
used[best] = 1;
sum += a[best];
}
cnt++;
}
return cnt;
};
const int WS[] = {8, 12, 20};
vector<ll> out;
int rounds = 300;
for (int w : WS) {
mt19937 rng(w * 2654435761u + 30u);
int bad2 = 0, bad3 = 0;
for (int r = 0; r < rounds; r++) {
int n = 6 + (int)(rng() % 3); // 6 ~ 8 件
vector<int> a(n);
for (int i = 0; i < n; i++) a[i] = 1 + (int)(rng() % (unsigned)w);
if (fitK(a, w, 2) != bestK(a, w, 2)) bad2++;
if (fitK(a, w, 3) != bestK(a, w, 3)) bad3++;
}
out.push_back(bad2); out.push_back(bad3);
if (!CSV) printf("④ 同一份 best-fit(w = %2d,n = 6..8,%d 组):"
"每组最多 2 件时错 %d 次,放宽到最多 3 件时错 %d 次\n",
w, rounds, bad2, bad3);
}
row("capK", out);
}
/* ⑤ best-fit 在各档规模 / 值域上都没错(说明不是「没搜到」) */
{
const int NS[] = {4, 10, 30, 100};
vector<ll> out;
for (int n : NS) {
mt19937 rng(n * 40503u + 9u);
int bad = 0;
for (int r = 0; r < 300; r++) {
vector<int> a = gen(rng, n, 100, 5);
if (solveFit(a, 100) != solve(a, 100)) bad++;
}
out.push_back(bad);
if (!CSV) printf("⑤ w = 100,P_i ∈ [5, 100],n = %3d:best-fit 和正解不同 %d / 300\n", n, bad);
}
row("fitN", out);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1094Gen.cpp数据生成器
// P1094 对拍生成器:`./p1094Gen <seed> [n 上限] [w 下限] [w 上限]`
// 默认 `n ≤ 10`、`w ∈ [80, 200]`、`P_i ∈ [5, w]` —— ★ 值域完全照题面。
//
// ⚠ 默认的 `n` 只到 10,因为参照物是**枚举所有分组方案**的暴力(跑不动大的)。
// ★ 而这一页的重点恰好不在 n 上:三个错法里最难抓的那个(best-fit)
// 的抓获率主要由 **P_i 相对 w 的分布**决定 —— 页面第 ④ 步拧的是那个旋钮。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int nHi = argc > 2 ? atoi(argv[2]) : 10;
int wLo = argc > 3 ? atoi(argv[3]) : 80;
int wHi = argc > 4 ? atoi(argv[4]) : 200;
nHi = max(1, min(30000, nHi));
wLo = max(2, min(200, wLo));
wHi = max(wLo, min(200, wHi));
mt19937 rng(seed * 2654435761u + 5u);
int w = wLo + (int)(rng() % (unsigned)(wHi - wLo + 1));
int n = (int)(rng() % (unsigned)nHi) + 1;
printf("%d\n%d\n", w, n);
for (int i = 0; i < n; i++)
printf("%d\n", 5 + (int)(rng() % (unsigned)(w - 5 + 1))); // 题面:5 ≤ P_i ≤ w
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

8一页纸

关键的一步 排序 + 对撞双指针,最贵的配最便宜的(交换论证两句话)
哪一版能 AC p1094.cppO(n log n)
这一页的主线 ★★★ 题面「每组最多两件」是命门 —— 有它这题是最大匹配(好几种贪心都对),
没它就是装箱问题(NP 难)
怎么称出它的重量 同一份 best-fit、同一批数据,上限 2 → 3:错的组数 0 / 0 / 0 → 5 / 7 / 4
⚠ 被实测打回的一段 草稿把 best-fit 当错法 —— 穷举 116 050一个反例都没有
⚠⚠ 而「搜不到」是配了自检才敢写的(同一段搜索对「相邻两两配」立刻搜到 1 1 2 3
真错法 l < r(漏掉最后单独那件,被抓 191)/ 相邻两两配(被抓 134)
★ 触发条件 「最后停在 l == r」191 轮 ≡ l < r 被抓 191 轮,一个不差(第四次量这件事)
参照物 枚举所有分组方案(不排序不贪心)—— 400 组不一致 0 组
和算法无关的坑 输入是 wn,和常见格式相反(样例挡得住)