题单 · 习题解析

洛谷 P5365 [SNOI2017] 英雄联盟

★★★ 「乘积不截断必溢出」是我的草稿,被实测打回来了 —— 随机 2000 组 + 顶格四档 M 全部相同,而且两行能证(答案下标之前的 f 都 < M)

原题:洛谷 P5365出自 第 24 章 完全背包与多重背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

正在上大学的小皮球热爱英雄联盟这款游戏,而且打的很菜,被网友们戏称为「小学生」。

现在,小皮球终于受不了网友们的嘲讽,决定变强了,他变强的方法就是:买皮肤!

小皮球只会玩 N 个英雄,因此,他也只准备给这 N 个英雄买皮肤,并且决定,以后只玩有皮肤的英雄

N 个英雄中,第 i 个英雄有 Kᵢ 款皮肤,价格是每款 Cᵢ Q 币(同一个英雄的皮肤价格相同)。

为了让自己看起来高大上一些,小皮球决定给同学们展示一下自己的皮肤,展示的思路是这样的: 对于有皮肤的每一个英雄,随便选一个皮肤给同学看。

比如,小皮球共有 5 个英雄,这 5 个英雄分别有 0, 0, 3, 2, 4 款皮肤, 那么,小皮球就有 3 × 2 × 4 = 24 种展示的策略。

现在,小皮球希望自己的展示策略能够至少达到 M,请问,小皮球至少要花多少钱呢?

输入格式

第一行,两个整数 N, M。第二行,N 个整数 Kᵢ。第三行,N 个整数 Cᵢ

输出格式

一个整数,表示小皮球达到目标最少的花费。

说明/提示

样例解释:每一个英雄都只有 4 款皮肤,每款皮肤 2 Q 币,那么每个英雄买 3 款, 3 × 3 × 3 ≥ 24,共花费 6 × 3 Q 币。

数据范围:共 10 组数据,第 i 组数据满足 N ≤ max(5, log₂⁴ i)。 100% 的数据:M ≤ 10¹⁷1 ≤ Kᵢ ≤ 101 ≤ Cᵢ ≤ 199保证有解。

输入输出样例

输入

3 24
4 4 4
2 2 2

输出

18

三个英雄各有 4 款皮肤、每款 2 Q 币,要凑够 24 种策略 ⇒ 每人买 3 款(3×3×3 = 27 ≥ 24), 花 9 × 2 = 18

⚠⚠ 这一组样例把本页三个错法全放过了(都输出 18)—— 和同章的 P1776(四个错法全挡住)正好是两个极端。

1★ 关键的一步:花费当容量,策略数当价值

这道题的门槛在看出它是个背包。翻译一遍:

    每个英雄 = 一个「组」,组内的选项是「买 0 / 1 / 2 / … / K[i] 款」
    选一个选项要花 k × C[i]                      <- 容量
    选完之后策略数乘上 k                          <- 价值(乘法,不是加法)
    问:策略数 ≥ M 时,容量最少是多少

分组背包(每组最多选一个选项),只是「价值」的合并方式从加法换成了乘法。

p5365.cpp★ 这一版就能 AC
// P5365 [SNOI2017] 英雄联盟 —— ★ 这一版就能 AC
//
// 题意:N 个英雄,第 i 个有 K[i] 款皮肤、每款 C[i] Q 币。
// 给第 i 个英雄买 k 款,展示策略数就乘上 k(买 0 款的英雄不参与展示)。
// 要让**策略数 ≥ M**,问最少花多少钱。
//
// ★ 关键的一步(一):**把「花费」当容量、「策略数」当价值** ——
// 这是一个**分组背包**:每个英雄是一组,组内选「买几款」(0 .. K[i]),最多选一个。
// f[j] = 花不超过 j 元时,能达到的**最大策略数**
// f[j] = max(f[j], f[j − k·C[i]] × k) k = 2 .. K[i]
// 答案 = 最小的 j 使 f[j] ≥ M。
//
// ★ 关键的一步(二):**k = 1 永远不用考虑** —— 买 1 款和买 0 款对乘积都是 ×1,
// 而买 1 款要多花 C[i]。⇒ 内层从 k = 2 起。
// (反过来,**强制每个英雄至少买 1 款**是一个真实的错法,见解析页第 ③ 步。)
//
// ★★★ 关键的一步(三):**乘积必须截断**。
// N 顶格 121、每个英雄 10 款 ⇒ 策略数最大 10^121,`long long` 装不下十分之一。
// 而我们只关心「有没有到 M」⇒ 把每次乘出来的值**和 M 取 min** 就行。
// ⇒ 不截断会溢出成负数,答案当场错(解析页第 ④ 步)。
//
// ⚠ 容量上界是算出来的:Σ K[i]·C[i] ≤ 121 × 10 × 199 = 240 790。
// ⚠ M ≤ 10^17 ⇒ 读 M 必须用 long long。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int n;
ll M;
if (!(cin >> n >> M)) return 0;
vector<int> K(n), C(n);
for (int i = 0; i < n; i++) cin >> K[i];
for (int i = 0; i < n; i++) cin >> C[i];
int cap = 0;
for (int i = 0; i < n; i++) cap += K[i] * C[i]; // ★ 容量上界:全买满
vector<ll> f(cap + 1, 1); // 一款都不买 ⇒ 策略数 1
for (int i = 0; i < n; i++)
for (int j = cap; j >= 0; j--) // 倒序:这一组只选一次
for (int k = 2; k <= K[i]; k++) { // ★ k = 1 和 k = 0 等价,跳过
int cost = k * C[i];
if (cost > j) break;
ll v = f[j - cost] * k;
if (v > M) v = M; // ★★★ 截断,否则 10^121 会溢出
f[j] = max(f[j], v);
}
for (int j = 0; j <= cap; j++)
if (f[j] >= M) { cout << j << "\n"; return 0; }
cout << cap << "\n"; // 题面保证有解,走不到这儿
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 「买 0 款」和「买 1 款」对乘积是同一件事 —— 所以 k = 1 永远不用考虑

题面的样例解释里悄悄给了一个关键信息:「5 个英雄分别有 0, 0, 3, 2, 4 款皮肤, 那么共有 3 × 2 × 4 = 24 种策略」—— 买 0 款的英雄不参与展示,也就是对乘积贡献 ×1。

而买 1 款呢?那个英雄只有一款皮肤可展示 ⇒ 也是 ×1,可它多花了 C[i]

k = 1 严格劣于 k = 0,内层从 k = 2 起就行。 ⚠ 反过来说,「强制每个英雄至少买 1 款」是一个真实的错法(第 ③ 步)。

p5365Brute.cpp参照物:枚举每个英雄买几款

2★★ 容量上界和复杂度:两笔都要自己乘一遍

    容量上界 = Σ Kᵢ · Cᵢ ≤ 121 × 10 × 199 = 240 790        <- f 数组开这么大
    复杂度   = O(容量 × Σ Kᵢ)  ≈ 2.4 × 10⁵ × 1210 ≈ 3 × 10⁸

N 的上界来自题面那句 N ≤ max(5, log₂⁴ i)i ≤ 10log₂(10)⁴ ≈ 121.6。)

顶格实测(N=121K=10C=199M=10¹⁷
容量上界 ΣK·C 240 790
f 数组(long long 1881 KB
内层执行次数 260 921 133
本机毫秒 233(时限 2000 ⇒ 余量约 8.6 倍)
答案 21 293

3★★ 两个真错法(而官方样例一个都没挡住)

p5365Up.cpp错法一:分组背包外层写成正序
p5365Force1.cpp错法二:强制每个英雄至少买 1 款
★★ 抓获率:同一批旋钮,两个 bug 的方向又一次相反

每档 300 轮:

档位 N≤5, K≤4, C≤10 N≤6, K≤10, C≤199 N≤8, K≤10, C≤50 N≤4, K≤10, C≤199
外层正序被抓 157 240 251 198
强制买 1 款被抓 216 123 140 122

「外层正序」要英雄多、单价差别大才现形(同一个英雄被反复买才划算); 「强制买 1 款」要英雄多而 K 小才现形(那时候「一款都不买」的英雄才多)。 ⇒ 这已经是这两轮里第三次看到「同一个旋钮把两个 bug 推向相反方向」 (P1049P2925)。 ⇒ 一个生成器上的旋钮,至少要往两头各拧一次再下结论。

4★★★ 而「必须截断」这一条 —— 是我的草稿,被实测打回来了

草稿里我写的是:

N 顶格 121、每个英雄 10 款 ⇒ 策略数最大 10¹²¹long long 只到 9.2 × 10¹⁸不截断必溢出、必错。

p5365NoCap.cpp对照:把截断那一句删掉
随机 2000 组:截断版 vs 不截断版 不一致 0 组
顶格 N=121, K=10, C=199M10¹⁷ / 10¹⁸ / 3×10¹⁸ / 9×10¹⁸ 答案 21293 / 22686 / 23283 / 23880,★ 四档全部相同
★★★ 它凭什么不会错 —— 两行就能证(和 P1164 是同一个形状)

设答案下标是 J最小的、满足 f[J] ≥ M 的那个 j)。

  • 所有 j < J 的格子,按 J 的定义都有 f[j] < M
  • f[J] 只可能由某个 f[J − cost](下标更小)乘上 k ≤ 10 得来 ⇒ 被用到的中间值 < 10 M

题面 M ≤ 10¹⁷ ⇒ 用得上的值 < 10¹⁸ < 2⁶³ —— 连有符号 long long 都够。 (下标 > J 的格子确实会溢出,但从 0 往上扫时根本走不到那儿。)

⇒ 这和 P1164 那条同一个形状的论证: 「答案 ≥ 任何一个被用到的中间值」⇒ 题面给的答案上界,就是「类型够不够」的充分条件。

★★ 但结论不是「所以别写截断」: 截断让这份代码不依赖上面那段推理 —— 换一道 M 更大、或者 K 更大的题, 上面那个 10 M 的余量(9.2 倍)说没就没。 ⇒ 该写还是写,但要知道自己写它是为了「不用去想」,而不是「不写就错」。

5★ 第三个错法:M 用 int 读 —— 它连读入都过不去

p5365IntM.cpp错法三:M 用 int

题面写着 M ≤ 10¹⁷,而 int 只到 2.1 × 10⁹ —— 差 8 个数量级。

⚠ 而它的失败方式比「算错」更彻底:cin >> int 读到超范围的数会置错误位, 于是那句 if (!(cin >> n >> M)) return 0; 直接返回 —— 程序一个字都不输出

★ 触发条件是一条数值线M > 2³¹ − 1。 ⇒ 小数据上它和正解一模一样(本页默认档 M ≤ 10⁶,300 轮精确的 0), 而题面顶格必挂。 ⇒ 又一次「溢出的触发条件是一条数值线,生成器够不够是算术题」。

6度量程序和生成器

p5365Count.cpp度量程序(本页所有数字都出自它)
p5365Gen.cpp数据生成器

7一页纸

关键的一步 花费当容量、策略数当价值分组背包(价值用乘法合并)
哪一版能 AC p5365.cpp —— 顶格 2.6 亿次 / 233 毫秒(时限 2000)
★★ 一个白捡的剪枝 k = 1 永远不用考虑(对乘积和 k = 0 一样,却要多花 C[i]
两笔算术 容量上界 ΣK·C = 240 790;复杂度 O(容量 × ΣK) ≈ 3 × 10⁸
错法一 分组背包外层正序 ⇒ 同一个英雄被买好几轮;被抓 157~251
错法二 强制每个英雄至少买 1 款(样例解释里的 0, 0 就是反例);被抓 122~216
错法三 Mint读入直接失败、一个字都不输出;触发线 M > 2³¹−1
⚠⚠ 被打回的草稿 「不截断必溢出」是错的:随机 2000 组 + 顶格四档 M 全部相同
它凭什么对 答案下标之前的 f< M被用到的中间值 < 10M < 2⁶³(和 P1164 同形状)
★ 但截断还是要写 它让代码不依赖那段推理 —— 那个余量只有 9.2 倍
⚠ 官方样例 三个错法一个都没挡住(和同章 P1776 正好两个极端)