0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P5322,日期见页头。两边不一致时信原站。
题目描述
小 C 正在玩一款排兵布阵的游戏。在游戏中有 n 座城堡,每局对战由两名玩家来争夺这些城堡。
每名玩家有 m 名士兵,可以向第 i 座城堡派遣 aᵢ 名士兵去争夺这个城堡,使得总士兵数不超过 m。
如果一名玩家向第 i 座城堡派遣的士兵数严格大于对手派遣士兵数的两倍,
那么这名玩家就占领了这座城堡,获得 i 分。
现在小 C 即将和其他 s 名玩家两两对战,这 s 场对决的派遣士兵方案必须相同。
小 C 通过某些途径得知了其他 s 名玩家即将使用的策略,他想知道他应该使用什么策略来最大化自己的总分。
由于答案可能不唯一,你只需要输出小 C 总分的最大值。
输入格式
输入第一行包含三个正整数 s, n, m,分别表示除了小 C 以外的玩家人数、城堡数和每名玩家拥有的士兵数。
接下来 s 行,每行 n 个非负整数,表示一名玩家的策略。
其中第 i 个数 aᵢ 表示这名玩家向第 i 座城堡派遣的士兵数。
输出格式
输出一行一个非负整数,表示小 C 获得的最大得分。
说明/提示
对于 10% 的数据:s = 1,n ≤ 3,m ≤ 10;
对于 20% 的数据:s = 1,n ≤ 10,m ≤ 100;
对于 40% 的数据:n ≤ 10,m ≤ 100;
对于另外 20% 的数据:s = 1;
对于 100% 的数据:1 ≤ s ≤ 100,1 ≤ n ≤ 100,1 ≤ m ≤ 2 × 10⁴。
对于每名玩家,保证 aᵢ ≥ 0,Σaᵢ ≤ m。
输入输出样例(一)
输入
1 3 10 2 2 6
输出
3
一个对手,三座城堡,10 个兵。对手派了 2 2 6。
小 C 的最佳策略是向第 1、2 座城堡各派 5 名士兵:
5 > 2 × 2 两座都占下 ⇒ 1 + 2 = 3 分。第 3 座要 13 个兵,掏不起。
⚠ 这一组样例三个错法一个都没挡住 —— 全都打出 3。
输入输出样例(二)
输入
2 3 10 2 2 6 0 0 0
输出
8
两个对手:2 2 6 和 0 0 0。
最佳策略之一是 2 5 1:第 1 座 2 > 2×0 赢一个(1 分),第 2 座 5 > 2×2 且 5 > 0 赢两个(4 分),
第 3 座 1 > 2×0 赢一个(3 分)⇒ 8 分。
★ 而这一组把三个错法全挡住了(12 / 9 / 6)。 ⇒ 第 16 章 P1074 那条:官方给了几组就跑几组,它们不是同一件事的重复。
1第一版:贪心 —— 「分高的城堡先抢」
城堡 i 值 i 分,那当然从编号最大的城堡开始抢:把它抢到手要 2·max(b) + 1 个兵,
掏得起就掏。
// ✗ P5322 的第一版:贪心 —— 「分越高的城堡越值得抢」。//// 城堡 i 值 i 分,所以从编号最大的城堡开始,一座一座地「能全赢就全赢」:// 需要 `2·max(b) + 1` 个兵,掏得起就掏,掏不起就跳过。//// ⚠ 它一次错两处:// ① 不一定要**全赢** —— 只赢一部分对手往往便宜得多、也划算得多;// ② 高分城堡不一定划算 —— 分数是线性的(`i`),而费用可以差好几十倍。
#include <bits/stdc++.h>using namespace std;
int main() { int s, n, m; if (scanf("%d %d %d", &s, &n, &m) != 3) return 0; vector<vector<int>> b(n + 1, vector<int>(s)); for (int j = 0; j < s; j++) for (int i = 1; i <= n; i++) if (scanf("%d", &b[i][j]) != 1) return 0;
int left = m, sum = 0; for (int i = n; i >= 1; i--) { // 从分最高的城堡开始 int need = 0; for (int j = 0; j < s; j++) need = max(need, 2 * b[i][j] + 1); if (need <= left) { left -= need; sum += s * i; } } printf("%d\n", sum); return 0;}点「运行 ▶」看结果
① 不一定要全赢 —— 只赢掉最弱的几个对手往往便宜得多;
② 分高的城堡不一定划算 —— 分数只是线性的 i,而费用能差几十倍。
| 300 轮被抓 | 100 / 300 |
| 错的时候平均少 | ★ 56.21% |
| 最差 | ★ 100.00%(一分没拿到) |
⇒ 对照第 20 章 P1048(性价比贪心:81% 的轮次是对的,错时平均只差 10.96%) 和本章 P1507(错时平均少 8.64% ~ 18.16%): ★★ 同样叫「贪心」,错的幅度差一个数量级 —— 「贪心会不会 WA」和「WA 得多难看」是两件事。
2★★ 关键的一步(一):每座城堡只有 s + 1 种「有意义的派兵数」
b₁ ≤ b₂ ≤ … ≤ b_s ← s 个对手在第 i 座城堡的兵力,排好序
想赢下最弱的 k 个 ⇒ 最少派 2·b_k + 1 个兵 (题面:严格大于两倍)
得分 = k × i派别的数目呢?——要么白花,要么多赢不了人:
派 x 且 2b_k < x ≤ 2b_(k+1) 时,赢的人数和派 2b_k + 1 一模一样,却多花了兵。
⇒ 于是「向这座城堡派多少兵」这个连续的选择,缩成了 s 件互斥的物品。
3★★ 关键的一步(二):每座城堡就是一组 —— 分组背包
一座城堡只能选一种派法 ⇒ 组内互斥 ⇒ 第 25 章下半场那三层循环,原样搬过来。
// P5322 [BJOI2019] 排兵布阵 —— ★ 这一版就能 AC。//// ★★ 关键的一步(一):**每座城堡只有 s + 1 种「有意义的派兵数」。**// 把 s 个对手在第 i 座城堡的兵力排好序(b₁ ≤ b₂ ≤ … ≤ b_s),// 想赢下前 k 个对手,最省的派法就是 `2·b_k + 1`(题面写的是**严格**大于两倍)。// 派别的数目要么白花兵,要么多赢不了人。//// ★★ 关键的一步(二):于是**每座城堡就是一组**,组内 s 件互斥物品:// 第 k 件:费用 `2·b_k + 1`,价值 `k × i`(赢 k 个对手,每个给 i 分)// 然后就是第 25 章下半场那三层循环:组 → 容量倒序 → 组内枚举。//// 复杂度 `O(nsm)`:顶格 100 × 100 × 2×10⁴ = 2 × 10⁸ —— 这道题就是卡着这个量级过的。
#include <bits/stdc++.h>using namespace std;
int main() { int s, n, m; if (scanf("%d %d %d", &s, &n, &m) != 3) return 0; vector<vector<int>> b(n + 1, vector<int>(s)); // b[城堡][对手] for (int j = 0; j < s; j++) for (int i = 1; i <= n; i++) if (scanf("%d", &b[i][j]) != 1) return 0;
vector<int> f(m + 1, 0); for (int i = 1; i <= n; i++) { sort(b[i].begin(), b[i].end()); // ★ 排完序,「赢前 k 个」才有意义 for (int j = m; j >= 0; j--) for (int k = 1; k <= s; k++) { // 组内枚举,必须在最里层 int cost = 2 * b[i][k - 1] + 1; // ★ 严格大于两倍 ⇒ 要多一个兵 if (cost > j) break; // 费用随 k 单调不减,超了就不用再看 f[j] = max(f[j], f[j - cost] + k * i); } } printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
内层执行次数 n × s × m |
顶格 100 × 100 × 2×10⁴ = ★ 2 × 10⁸ |
答案上界 s × (1+2+…+n) |
100 × 5050 = 505 000(int 余量 4252 倍) |
f 数组 |
78 KB |
⇒ 那个 2 × 10⁸ 是这道题「进阶」的地方:能不能过,取决于内层那几行有多干净
(正解里那句 if (cost > j) break; 就是靠「费用随 k 单调不减」省下来的)。
4★ 「严格大于两倍」少写一个 +1
// ✗ P5322 的第一个坑:把「**严格**大于两倍」读成了「大于等于两倍」。//// 题面那个「严格」两个字是加粗的:// 「如果一名玩家向第 i 座城堡派遣的士兵数**严格**大于对手派遣士兵数的两倍,那么这名玩家就占领了这座城堡」。//// 这一版把费用写成 `2·b_k`(而不是 `2·b_k + 1`),于是每座城堡都少花 1 个兵。// ★ 它最刺眼的地方在 `b = 0`:对手一个兵都不派时,它认为**我也不用派**就赢了。
#include <bits/stdc++.h>using namespace std;
int main() { int s, n, m; if (scanf("%d %d %d", &s, &n, &m) != 3) return 0; vector<vector<int>> b(n + 1, vector<int>(s)); for (int j = 0; j < s; j++) for (int i = 1; i <= n; i++) if (scanf("%d", &b[i][j]) != 1) return 0;
vector<int> f(m + 1, 0); for (int i = 1; i <= n; i++) { sort(b[i].begin(), b[i].end()); for (int j = m; j >= 0; j--) for (int k = 1; k <= s; k++) { int cost = 2 * b[i][k - 1]; // ✗ 少了那个 + 1 if (cost > j) break; f[j] = max(f[j], f[j - cost] + k * i); } } printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
样例一放过(3),样例二打出 9(挡住)。
| 300 轮被抓 | 168 / 300 |
| 而「有对手在某座城堡派 0 兵」的轮数 | 247 / 300 |
★ b = 0 是这个错法最刺眼的地方:对手一个兵都不派时,它认为我也不用派就赢了。
⇒ 又一次「触发条件 ≠ 抓获数」(247 ↔ 168,差 1.47 倍)。
5★★ 组内那句 sort 忘了 —— 而这个错法配了一个算得出来的 0
「派 2·b_k + 1 个兵能赢 k 个对手」这句话,只在 b 已经从小到大排好时才成立。
// ✗ P5322 的第二个坑:组内那 s 件物品**没排序**。//// 「派 `2·b_k + 1` 个兵能赢 k 个对手」这句话,只有在 b 已经**从小到大排好**时才成立// —— 它说的是「赢下最弱的那 k 个」。//// 这一版照输入顺序直接拿 b[k-1] 当第 k 件物品的费用,// 于是「费用」和「赢几个」对不上号:花了赢 3 个的钱,可能只赢得了 1 个。// ⚠ 而它**不一定错**:输入本来就有序的时候,它和正解一模一样(页面上量了这一档)。
#include <bits/stdc++.h>using namespace std;
int main() { int s, n, m; if (scanf("%d %d %d", &s, &n, &m) != 3) return 0; vector<vector<int>> b(n + 1, vector<int>(s)); for (int j = 0; j < s; j++) for (int i = 1; i <= n; i++) if (scanf("%d", &b[i][j]) != 1) return 0;
vector<int> f(m + 1, 0); for (int i = 1; i <= n; i++) { // ✗ 这里少了一句 sort for (int j = m; j >= 0; j--) for (int k = 1; k <= s; k++) { int cost = 2 * b[i][k - 1] + 1; if (cost <= j) f[j] = max(f[j], f[j - cost] + k * i); } } printf("%d\n", f[m]); return 0;}点「运行 ▶」看结果
| 生成器 | 「没排序」被抓 | 而「真有某座城堡那一列没排好」 |
|---|---|---|
| 照题面随机 | 89 / 300 | 137 / 300 |
| ★ 每座城堡那一列本来就有序(自检档) | ★ 精确的 0 | 0 |
★ 下面那个 0 不用跑就知道:那一档里 sort 是恒等变换。
⇒ 它同时是那段对拍代码的自检(P2240 / P1094 立的规矩)。
⚠ 注意排的是列(同一座城堡、不同对手),不是输入里的行 —— 这一点在生成器里也标了。
6★★★ 题面最后那句 Σaᵢ ≤ m 是对「输入」的保证 —— 顺手随机几乎一定违反它
数据范围最后一行:「对于每名玩家,保证 aᵢ ≥ 0,Σaᵢ ≤ m」。
写生成器时最自然的动作是「每座城堡独立随机 0 ~ m」。它满足那句保证的比例是:
n = 3、m = 10(10% 那一档的规模) |
21.52% |
顶格 n = 100、m = 2 × 10⁴ |
★ 0 / 100 000 |
⇒ 照题面顶格的规模顺手随机,造出的数据一组都不合法 —— 测的全是题目不会给的输入。 ★ 和第 24 章 P2347 那句「其总重 ≤ 1000」是同一类坑(那里是 6.3%), 而这一页把它推到了 0%:⇒ 看到「保证 Σ… ≤ …」,先算一下随手造的数据有多大概率满足它。
⇒ 正确的造法是分配而不是独立随机:把不超过 m 个兵随机撒到 n 座城堡上。
7★ 部分分那五行,把对拍档也写好了
| 拿它干什么 | ||
|---|---|---|
10%:s = 1,n ≤ 3,m ≤ 10 |
派兵方案只有 C(13,3) 种 |
★ 枚举「每座城堡派几个兵」的暴力跑得动 |
40%:n ≤ 10,m ≤ 100 |
多对手、小规模 | 验「组内 s 件」这一步 |
100%:n, s ≤ 100,m ≤ 2×10⁴ |
2 × 10⁸ |
定死复杂度 |
⇒ 本页那个参照物用的就是第一行那一档(生成器默认 s ≤ 3, n ≤ 3, m ≤ 10)。
这是第 24 章 P1776 那条的又一次:部分分是出题人替你写好的对拍档。
8度量程序、生成器和参照物
9一页纸
| ★★ 关键的一步(一) | 每座城堡只有 s + 1 种有意义的派兵数:排好序之后「赢前 k 个」要 2·b_k + 1 个兵 |
| ★★ 关键的一步(二) | 每座城堡 = 一组,组内 s 件互斥物品(价值 k × i)⇒ 分组背包 |
| 规模 | n × s × m 顶格 2 × 10⁸;答案上界 505 000(int 够);f 78 KB |
| 第一版:贪心 | 被抓 100 / 300,★ 错的时候平均少 56%、最差 100% —— 和 P1048 那种「只错一点点」的贪心完全不是一回事 |
| 漏掉「严格」那个 +1 | 被抓 168 / 300(触发条件 247 轮);b = 0 时最刺眼 |
| ★★ 组内忘了 sort | 89 / 300;★ 「每列本来就有序」那一档是算得出来的精确 0(自检) |
★★★ Σaᵢ ≤ m |
顺手独立随机满足它:小档 21.52%,★ 顶格 0 / 100 000 ⇒ 生成器要分配不要独立随机 |
| ★ 两组官方样例 | 第一组三个错法全放过,第二组全挡住 —— 给了几组就跑几组 |