题单 · 习题解析

洛谷 P5322 [BJOI2019] 排兵布阵

★★ 每座城堡只有 s+1 种有意义的派兵数 ⇒ 分组背包;★★★ 而题面那句「Σaᵢ ≤ m」顶格顺手随机满足它的比例是 0 / 100000

原题:洛谷 P5322出自 第 25 章 二维费用与分组背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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 = 1n ≤ 3m ≤ 10; 对于 20% 的数据:s = 1n ≤ 10m ≤ 100; 对于 40% 的数据:n ≤ 10m ≤ 100; 对于另外 20% 的数据:s = 1; 对于 100% 的数据:1 ≤ s ≤ 1001 ≤ n ≤ 1001 ≤ 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 60 0 0

最佳策略之一是 2 5 1:第 1 座 2 > 2×0 赢一个(1 分),第 2 座 5 > 2×25 > 0 赢两个(4 分), 第 3 座 1 > 2×0 赢一个(3 分)⇒ 8 分。

★ 而这一组把三个错法全挡住了(12 / 9 / 6)。 ⇒ 第 16 章 P1074 那条:官方给了几组就跑几组,它们不是同一件事的重复。

1第一版:贪心 —— 「分高的城堡先抢」

城堡 ii 分,那当然从编号最大的城堡开始抢:把它抢到手要 2·max(b) + 1 个兵, 掏得起就掏。

p5322Greedy.cpp✗ 第一版:先抢分高的城堡,能全赢就全赢
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 它一次错两处

① 不一定要全赢 —— 只赢掉最弱的几个对手往往便宜得多; ② 分高的城堡不一定划算 —— 分数只是线性的 i,而费用能差几十倍。

★ 量一遍:这个贪心和背包题里那些「只错一点点」的贪心完全不是一回事
300 轮被抓 100 / 300
错的时候平均少 56.21%
最差 100.00%(一分没拿到)

⇒ 对照第 20 章 P1048(性价比贪心:81% 的轮次是对的,错时平均只差 10.96%) 和本章 P1507(错时平均少 8.64% ~ 18.16%): ★★ 同样叫「贪心」,错的幅度差一个数量级 —— 「贪心会不会 WA」和「WA 得多难看」是两件事。

2★★ 关键的一步(一):每座城堡只有 s + 1 种「有意义的派兵数」

★ 把 s 个对手在这座城堡的兵力排好序,答案就只剩 s + 1 个候选
    b₁ ≤ b₂ ≤ … ≤ b_s          ← s 个对手在第 i 座城堡的兵力,排好序
    想赢下最弱的 k 个 ⇒ 最少派 2·b_k + 1 个兵      (题面:严格大于两倍)
    得分 = k × i

派别的数目呢?——要么白花,要么多赢不了人: 派 x2b_k < x ≤ 2b_(k+1) 时,赢的人数和派 2b_k + 1 一模一样,却多花了兵。

⇒ 于是「向这座城堡派多少兵」这个连续的选择,缩成了 s 件互斥的物品

3★★ 关键的一步(二):每座城堡就是一组 —— 分组背包

一座城堡只能选一种派法 ⇒ 组内互斥 ⇒ 第 25 章下半场那三层循环,原样搬过来。

p5322.cpp★ 这一版就能 AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 规模:这道题就是卡着量级过的
内层执行次数 n × s × m 顶格 100 × 100 × 2×10⁴ = ★ 2 × 10⁸
答案上界 s × (1+2+…+n) 100 × 5050 = 505 000int 余量 4252 倍)
f 数组 78 KB

⇒ 那个 2 × 10⁸ 是这道题「进阶」的地方:能不能过,取决于内层那几行有多干净 (正解里那句 if (cost > j) break; 就是靠「费用随 k 单调不减」省下来的)。

4★ 「严格大于两倍」少写一个 +1

p5322Eq.cpp✗ 费用写成 2·b(漏了严格那个 +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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

样例一放过(3),样例二打出 9(挡住)。

300 轮被抓 168 / 300
而「有对手在某座城堡派 0 兵」的轮数 247 / 300

b = 0 是这个错法最刺眼的地方:对手一个兵都不派时,它认为我也不用派就赢了。 ⇒ 又一次「触发条件 ≠ 抓获数」(247 ↔ 168,差 1.47 倍)。

5★★ 组内那句 sort 忘了 —— 而这个错法配了一个算得出来的 0

「派 2·b_k + 1 个兵能赢 k 个对手」这句话,只在 b 已经从小到大排好时才成立

p5322NoSort.cpp✗ 组内 s 件物品没排序
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 两档(各 300 轮)
生成器 「没排序」被抓 而「真有某座城堡那一列没排好」
照题面随机 89 / 300 137 / 300
★ 每座城堡那一列本来就有序(自检档) 精确的 0 0

★ 下面那个 0 不用跑就知道:那一档里 sort 是恒等变换。 ⇒ 它同时是那段对拍代码的自检P2240 / P1094 立的规矩)。 ⚠ 注意排的是(同一座城堡、不同对手),不是输入里的行 —— 这一点在生成器里也标了。

6★★★ 题面最后那句 Σaᵢ ≤ m 是对「输入」的保证 —— 顺手随机几乎一定违反它

数据范围最后一行:「对于每名玩家,保证 aᵢ ≥ 0Σaᵢ ≤ m」。

写生成器时最自然的动作是「每座城堡独立随机 0 ~ m」。它满足那句保证的比例是:

n = 3m = 10(10% 那一档的规模) 21.52%
顶格 n = 100m = 2 × 10⁴ 0 / 100 000

照题面顶格的规模顺手随机,造出的数据一组都不合法 —— 测的全是题目不会给的输入。 ★ 和第 24 章 P2347 那句「其总重 ≤ 1000」是同一类坑(那里是 6.3%), 而这一页把它推到了 0%:⇒ 看到「保证 Σ… ≤ …」,先算一下随手造的数据有多大概率满足它。

⇒ 正确的造法是分配而不是独立随机:把不超过 m 个兵随机撒到 n 座城堡上。

7★ 部分分那五行,把对拍档也写好了

拿它干什么
10%:s = 1n ≤ 3m ≤ 10 派兵方案只有 C(13,3) 枚举「每座城堡派几个兵」的暴力跑得动
40%:n ≤ 10m ≤ 100 多对手、小规模 验「组内 s 件」这一步
100%:n, s ≤ 100m ≤ 2×10⁴ 2 × 10⁸ 定死复杂度

⇒ 本页那个参照物用的就是第一行那一档(生成器默认 s ≤ 3, n ≤ 3, m ≤ 10)。 这是第 24 章 P1776 那条的又一次:部分分是出题人替你写好的对拍档。

8度量程序、生成器和参照物

p5322Count.cpp度量程序(本页所有数字都出自它)
p5322Gen.cpp数据生成器
p5322Brute.cpp参照物:枚举每座城堡派几个兵(300 轮不一致 0 轮)

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 ⇒ 生成器要分配不要独立随机
★ 两组官方样例 第一组三个错法全放过,第二组全挡住 —— 给了几组就跑几组