题单 · 习题解析

洛谷 P1164 小 A 点菜

★★★ 题面那句「答案 ≤ 2³¹−1」正好是 32 位够用的充分条件(两行能证);而「读反 N / M」在官方样例上是恒等变换 —— 4 = 4,精确的 0

原题:洛谷 P1164出自 第 23 章 01 背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

uim 神犇拿到了 uoi 的 ra(镭牌)后,立刻拉着基友小 A 到了一家……餐馆,很低端的那种。

uim 指着墙上的价目表(太低级了没有菜单),说:“随便点”。

题目描述

不过 uim 由于买了一些书,口袋里只剩 M(0 < M ≤ 10000)

餐馆虽低端,但是菜品种类不少,有 N(1 ≤ N ≤ 100),第 i 种卖 aᵢ(0 < aᵢ ≤ 1000)。 由于是很低端的餐馆,所以每种菜只有一份

小 A 奉行“不把钱吃光不罢休”的原则,所以他点单一定刚好把 uim 身上所有钱花完。 他想知道有多少种点菜方法。

由于小 A 肚子太饿,所以最多只能等待 1 秒。

输入格式

第一行两个整数 NM,分别表示菜品种类uim 身上的钱数

第二行 N 个正整数 aᵢ可能有重复),用空格隔开,分别表示每种菜的价格。

输出格式

一个正整数,表示点菜方案数,保证答案的范围在 [0, 2³¹ − 1] 之内(不超过 C/C++ 的 int 范围)。

说明/提示

2020.8.29,增添一组 hack 数据 by @yummy

输入输出样例

输入

4 4
1 1 2 2

输出

3

四道菜价格 1 1 2 2,要正好花掉 4 元:1+1+2 有两种(两个 2 各算一种)、2+2 有一种 ⇒ 3 种。

★ 这组样例专门在考「价格相同的两道菜是不同的两道菜」 —— 不能去重。 ⚠ 而它同时在结构上问不出本页最后一个错法(第 ⑤ 步):NM 都是 4。

1★ 关键的一步:把 max 换成加法

这道题是 01 背包计数形态。第 23 章第 ⑫ 步 给过这个套路,一句话就是它的全部:

★ 求最大值 → max;求方案数 → 加法;而初始化必须跟着改
求最大价值 求方案数(这道题)
状态 f[j] = 容量 j 时的最大价值 f[j] = 恰好花掉 j 元的点法数
转移 f[j] = max(f[j], f[j-a] + w) f[j] += f[j-a]
初值 全 0 f[0] = 1,其余 0

那个 f[0] = 1 不是凑数:「一分钱不花」本身就是一种点法, 而整张表的方案数都是从这一个 1 一层层长出来的。忘了它,全表恒为 0(第 ③ 步)。

p1164.cpp★ 这一版就能 AC
// P1164 小 A 点菜 —— ★ 这一版就能 AC
//
// 题意:N 道菜(每道只有一份),价格 a[i];恰好把 M 元**花光**,问有多少种点法。
//
// ★ 关键的一步:把 01 背包的 max 换成加法。
// [第 23 章第 ⑫ 步](/ch/23-knapsack-01/)讲过这个套路 ——
// 问「最大价值」就 max,问「有多少种方案」就把 max 换成 `+`,
// 而初始化从「f 全 0」换成 **f[0] = 1,其余 0**。
//
// f[j] = 恰好花掉 j 元的点法数
// f[j] += f[j - a[i]] (倒序,每道菜只有一份)
// f[0] = 1 ← 「一分钱不花」也是一种点法(什么都不点)
//
// ⚠ 那个 f[0] = 1 不是凑数:整张表的方案数都是从它一层层长出来的。
// 忘了它,全表恒为 0(见 p1164Zero.cpp)。
//
// ★ 关于 int 够不够:题面写着「保证答案在 [0, 2^31-1] 之内」——
// 这句话**正好是 int 够用的充分条件**,而且能证(解析页第 ⑤ 步)。
// ⚠ 但「用得上的格子不溢出」不等于「所有格子都不溢出」,所以这里仍写 long long。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
if (!(cin >> n >> m)) return 0; // ⚠ 顺序是「菜品种类 N,钱数 M」
vector<long long> f(m + 1, 0);
f[0] = 1; // ★ 一分钱不花:一种点法
for (int i = 0; i < n; i++) {
int a; cin >> a;
for (int j = m; j >= a; j--) // 倒序:每道菜只有一份
f[j] += f[j - a];
}
cout << f[m] << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

复杂度 O(NM),顶格 100 × 10000 = 10⁶ —— 时限 1 秒,绰绰有余。

作为对照,二维版长这样(它压根不需要「倒序」这个概念):

p1164Dp2.cpp对照:二维,没有倒序这回事
★ 一维倒序的全部意义,就是把「读上一行」模拟出来

二维版右边读的全是 f[i-1][…]根本不可能读到本行刚写的值。 一维压掉那一维之后,「上一行」和「这一行」挤在同一个数组里 —— 倒序就是唯一能保证「读到的还是上一行」的遍历方式。

⇒ 想不起来该正序还是倒序时,回去看二维版:它读的是哪一行,一维就要保证读到哪一行。

第一版的参照物是 DFS 枚举子集(2ᴺ,顶格当然不行,但小数据上它和 DP 一行代码都不共享):

p1164Dfs.cpp参照物:2^N 枚举子集

2三个错法,官方样例一口气挡住了三个

p1164Up.cpp错法一:一维正序
p1164Zero.cpp错法二:忘了 f[0] = 1
p1164Le.cpp错法三:把「恰好」做成了「不超过」
官方样例 4 4 / 1 1 2 2 输出 挡住了吗
正解 3 ——
一维正序 14
忘了 f[0] = 1 0
「不超过 M」 13 必被挡住
★ 读反 NM(第 ⑤ 步) 3 放过
★ 「正序」照例不是随机地错 —— 它精确地在数另一道题的方案
随机 300 组:正序的答案 vs 「每道菜可以点任意多份」的方案数 300 / 300 逐组相等

⇒ 它解的是完全背包的计数版本。样例上 14 > 3,是因为 「点两份 1 元的菜」这种非法方案也被数进去了。

★ 顺带说清「不超过」那个错法为什么一次都逃不掉(对拍 300 / 300): Σf[j] 里至少还多着一个 f[0] = 1(什么都不点),所以只要 M ≥ 1, 它就恒大于 f[M]。这不是概率,是一句不等式。

3★★ 「忘了 f[0] = 1」:它漏掉的轮数 ≡ 答案本来就是 0 的轮数

这个 bug 有一条白送的推论:它的输出恒等于 0,和输入完全无关。 ⇒ 它唯一能蒙混过关的方式,就是正确答案本来就是 0

数一数两边(N ≤ 10、价格 ≤ 10 固定,只拧 M,各 300 轮):

M 的上限 10 30 100
被抓的轮数 186 182 75
漏掉的轮数 114 118 225
「正确答案本来就是 0」的轮数 114 118 225
★★ 三档全部一个不差 —— 而这张表还顺手指出了生成器该往哪拧

「漏掉的轮数 ≡ 答案为 0 的轮数」三档全中 ⇒ 等于证明了 「只要答案不是 0,它一定被抓」。

⚠ 而更有用的是反过来读这张表:M 上限从 10 拉到 100,抓获率从 186 掉到 75 —— 掉下去的那一段全是「两版一起输出 0」(钱多菜少,凑不出来)。 这是第 14 章 P1746 那条「一致有两种:都算对了,和都没算」的第三次现场。

⇒ 生成器该照抄的是题面的比值M / 价格上限),不是绝对规模 —— 和 P1020 那条同一个道理。这一页默认档 M ≤ 30、价格 ≤ 10(比值 3), 就是为了让「凑得出来」的轮次占多数。

4★★★ 读反 N 和 M —— 而官方样例在结构上问不出这个问题

题面第一行是「NM」=「菜品种类身上的钱」。 而「先读钱、再读菜数」也很顺手(不少背包题的输入就是那个顺序)。

p1164Swap.cpp错法四:两个输入读反了
// P1164 错法四:把两个输入读反了 —— ★★ 而官方样例**在结构上问不出这个问题**
//
// 题面第一行是「N 和 M」=「菜品种类、身上的钱」。
// 而「先读钱、再读个数」也很顺手(很多背包题的输入就是那个顺序),于是有了这一版。
//
// ★★★ 官方样例是 `4 4` —— **N 和 M 恰好相等**,交换它们是**恒等变换**。
// ⇒ 这不是「概率低」,是**精确的 0**:这组样例问不出这个问题。
// 和[第 14 章 P1746](/sol/p1746/) 那个「两个点都在主对角线上」是同一件事。
//
// ⚠ 而读反之后还会再错一次:菜价那一行要读 N 个数,读成 M 个就会读少 / 读多。
// 这里如实照抄「读反了」的写法:n 和 m 换个位置,别的一个字不改。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
if (!(cin >> m >> n)) return 0; // ⚠ 反了:先读的其实是 N
vector<long long> f(m + 1, 0);
f[0] = 1;
for (int i = 0; i < n; i++) {
int a = 0;
if (!(cin >> a)) break; // 读不到就算了(数量对不上)
for (int j = m; j >= a; j--)
f[j] += f[j - a];
}
cout << f[m] << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它在官方样例上输出 3,和正解一模一样。

★★★ 这个 0 不是「概率低」,是恒等变换 —— 官方样例是 `4 4`

NM 恰好都是 4 ⇒ 交换它们什么也没做。这不是运气,是这组样例的结构:

「读反」被抓的轮数
默认档(N ≤ 10M ≤ 30)300 轮 192 / 300
★ 专门造的 N = M 那一档 300 轮 精确的 0 / 300

⇒ 这和第 14 章 P1746 那个「样例的两个点都在主对角线上 ⇒ 交换行列是恒等变换」 是同一件事:「它过了样例」的第三种原因 —— 这组样例在结构上问不出这个问题

★ 而那个「精确的 0」自带自检:同一份对拍代码在默认档抓了 192 次, 所以它不是空壳(P2240 那天定下的规矩:报「0 次」之前先证明这段代码是活的)。

⚠ 顺带:默认档漏掉的 108 轮里,只有 16 轮是「N 恰好等于 M」的恒等, 另外 92 轮是读反了、但答案碰巧相同。⇒ 「它有时候能对」和「它没错」是两句话。

5★★★ 题面那句「答案 ≤ 2³¹ − 1」到底保证了什么

题面在输出格式那行写着:「保证答案的范围在 [0, 2³¹−1] 之内(不超过 C/C++ 的 int 范围)。」

第 12 章那套分法,这是一条情报。而它比看上去给得多 —— 它正好是「32 位够用」的充分条件,并且这件事能证:

★★★ 两行的证明:答案 ≥ 任何一个被用到的中间值

对任意一道菜 i:把「凑出 M − aᵢ 的每一种点法」配上第 i 道菜, 就得到一种「凑出 M 的点法」,而且不同的原方案给出不同的新方案

答案 = f[M] ≥ f_{i−1}[M − aᵢ] —— 而 f_{i−1}[M − aᵢ] 正是转移时读到的那一格。 ⇒ 只要答案 ≤ 2³¹−1DP 路径上用得上的格子就全都装得下

实测(N = 100M = 10000、价格 ∈ [900, 1000]、194 组满足题面保证的顶格数据):

「用得上的中间值 > 答案」出现的次数 0
32 位版 vs long long 版不一致的组数 0
p1164Uint.cpp32 位计数(拿来验上面那件事)
⚠⚠ 但「用得上的格子不溢出」不等于「所有格子都不溢出」

造一组:100 道菜全是 1 元,M = 10000

正确答案 0(一百块钱凑不出一万)
32 位版的输出 0(照样对)
中间超过 2³¹−1 的格子 87 个
最大的一格 f[50] = C(100,50) ★ 约 1.01 × 10²⁹(29 位)

⇒ 溢出真的发生了,只是那些格子谁也没读。 ⚠ 而「没读到」并不能让有符号溢出变成合法 —— 它仍然是未定义行为。 所以本页那份演示写的是 unsigned(无符号溢出是有定义的,才能复现), 而正解干脆写 long long「算完确认够用」和「没算过」是两回事。

★★ 顺带踩中一次:「顶格」要顶到题面的边上,而题面的边包括输出侧

想造顶格数据测一测,第一反应是「N = 100M = 10000、价格在 [1, 1000] 里随机」。

那样造出来的 200 组
答案越过 2³¹−1 的组数 200 / 200 —— 一组能用的都没有

这些输入题目根本不会给。 要造出真能用的顶格数据,得把价格抬到 [900, 1000] (菜贵了,凑法就少了)—— 那一档 200 组里有 194 组可用,最大答案 2.92 × 10⁷

⇒ 这是 P1090 那条的又一次现场: 自己造顶格数据时要连「输出侧的保证」一起满足,否则测的是一组题目不会给的输入。

6度量程序和生成器

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

7一页纸

关键的一步 01 背包的计数形态:max 换成 +,初值换成 f[0] = 1
哪一版能 AC p1164.cpp —— 一维倒序,O(NM) = 10⁶
想不起正序倒序 回去看二维版:它读的是哪一行,一维就要保证读到哪一行
错法一 一维正序 ⇒ 恒等于「每道菜可点任意多份」的方案数(300/300 逐组相等)
错法二 忘了 f[0] = 1恒输出 0;★ 漏掉的轮数 ≡ 答案为 0 的轮数(三档全中)
错法三 把「恰好花完」做成「不超过」
★★★ 错法四 读反 N / M —— 官方样例 4 4 让交换成了恒等变换(精确的 0);默认档 192/300
★★★ 题面那句保证 「答案 ≤ 2³¹−1」正好是 32 位够用的充分条件,两行能证(答案 ≥ 任何被用到的中间值)
⚠ 但仍要写 long long 用不上的格子照样溢出:100 道 1 元菜 / M=1000087 个格子越界,最大 10²⁹
⚠ 造顶格数据 价格 ∈ [1,1000] 随机 ⇒ 200/200 组的答案越过题面保证,一组都不能用