0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2347,日期见页头。两边不一致时信原站。
题目描述
设有 1g、2g、3g、5g、10g、20g 的砝码各若干枚(其总重 ≤ 1000),可以表示成多少种重量?
输入格式
输入方式:a₁ a₂ a₃ a₄ a₅ a₆
(表示 1g 砝码有 a₁ 个,2g 砝码有 a₂ 个,…,20g 砝码有 a₆ 个)
输出格式
输出方式:Total=N
(N 表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)
说明/提示
【题目来源】NOIP 1996 提高组第四题
输入输出样例
输入
1 1 0 0 0 0
输出
Total=3
一枚 1g、一枚 2g ⇒ 能称出 1、2、3 三种重量,输出 Total=3。
★ 这一组样例把本页三个错法全挡住了(Total=1000 / Total=4 / Total = 3)。
1★ 它是布尔版的多重背包:max 换成「或」
状态:f[j] = 「重量 j 称不称得出来」
转移:f[j] |= f[j − 这一堆的重量] (倒序,每堆只用一次)
边界:f[0] = true (一枚不用,重量 0)
答案:Σ f[j](j 从 1 数起 —— ⚠ 把「一枚不用」那一种去掉)
// P2347 [NOIP 1996 提高组] 砝码称重 —— ★ 这一版就能 AC//// 题意:1g / 2g / 3g / 5g / 10g / 20g 六种砝码各若干枚(**总重 ≤ 1000**),// 问能称出多少种不同的重量(**不包括一枚也不用的情况**)。//// ★ 它是**布尔版的多重背包**:状态不是「最大价值」而是「能不能凑出」。// f[j] |= f[j - 单堆重量]// 转移里的 max 换成了「或」,其余和[第 24 章](/ch/24-knapsack-multi/)一模一样。//// ★ 而这道题在题单里的位置是「**拿来验证拆完答案不变**」:// 总重 ≤ 1000 ⇒ 摊开也只有 10^6 次,拆不拆都能过。// ⇒ 所以它是个安全的地方,可以把「二进制拆分到底改没改答案」这件事**真验一遍**// (解析页第 ② 步:三种写法 300 组逐字节相同)。//// ⚠ 两个和算法无关、但一定要看清的地方:// ① 输出是 `Total=N`,**等号两边没有空格**;// ② 「不包括一个砝码也不用的情况」⇒ 答案是「可达重量数 **减 1**」(把 0 去掉)。
#include <bits/stdc++.h>using namespace std;
int main() { const int W[6] = { 1, 2, 3, 5, 10, 20 }; const int MAXW = 1000; vector<char> f(MAXW + 1, 0); f[0] = 1; // 一枚不用,重量 0
for (int i = 0; i < 6; i++) { int a; if (!(cin >> a)) return 0; for (int k = 1; a > 0; k <<= 1) { // ★ 二进制拆分 int take = min(k, a); a -= take; int cw = W[i] * take; if (cw > MAXW) continue; for (int j = MAXW; j >= cw; j--) // 倒序:这一堆只用一次 if (f[j - cw]) f[j] = 1; } }
int cnt = 0; for (int j = 1; j <= MAXW; j++) cnt += f[j]; // ⚠ 从 1 开始 —— 去掉「一枚不用」 printf("Total=%d\n", cnt); // ⚠ 等号两边没有空格 return 0;}点「运行 ▶」看结果
2★★ 题单说这道题「正好拿来验证拆完答案不变」—— 那就真验一遍
第 24 章的题单给这道题写的提示是:
数据小,拆不拆都能过 —— 正好拿来验证「拆完答案不变」。
第 24 章第 ⑬ 步证过二进制拆分能凑出 0 .. m 的每一个数,
但证明和实测是两件事。这道题规模小,正好可以把三种独立的实现摆在一起对:
| 随机 300 组:摊开 ≡ 二进制拆分 ≡ bitset | ★ 不一致 0 组 |
| 那 300 组里最大的答案 | 995(上界是 1000) |
顶格的一种:1000 枚 1g 砝码(总重正好 1000)。
| 内层执行次数 | |
|---|---|
| 摊开 | 1 000 000 |
| 二进制拆分(10 堆) | ★ 9 010 |
| 倍数 | 110 倍 |
⇒ 题单那句话是对的:10⁶ 次在 1 秒时限下随便过 —— 所以这道题是个安全的练手场。
★ 而同样一招在 P1776 上就是生死线(那道题摊开要 39.5 亿次)。
3★★ 两个错法,各配一条精确的等式
// P2347 错法二:把「一枚砝码也不用」也数进去了//// 题面在输出格式那行明写着「**但不包括一个砝码也不用的情况**」。// 这一版从 j = 0 开始数。//// ★ 它有一条白送的等式:**它的答案恒等于正解 + 1**(解析页第 ④ 步,300 / 300 逐组相等)。// ⇒ 说清楚之后,它的所有表现都是推论:任何一组数据它都错,而且永远只差 1。// ⇒ 于是它必然**被官方样例挡住**(Total=4 vs Total=3)——// 又一次「[样例挡住的都是「每组都错」的](/sol/p1223/)」。
#include <bits/stdc++.h>using namespace std;
int main() { const int W[6] = { 1, 2, 3, 5, 10, 20 }; const int MAXW = 1000; vector<char> f(MAXW + 1, 0); f[0] = 1;
for (int i = 0; i < 6; i++) { int a; if (!(cin >> a)) return 0; for (int k = 1; a > 0; k <<= 1) { int take = min(k, a); a -= take; int cw = W[i] * take; if (cw > MAXW) continue; for (int j = MAXW; j >= cw; j--) if (f[j - cw]) f[j] = 1; } }
int cnt = 0; for (int j = 0; j <= MAXW; j++) cnt += f[j]; // ⚠ 从 0 开始 printf("Total=%d\n", cnt); return 0;}点「运行 ▶」看结果
| 精确的等式 | 300 轮里成立 | 被抓 | |
|---|---|---|---|
| 内层正序 | ≡ 「六种砝码都无限枚」时的答案 | ★ 300 / 300 | 300 / 300 |
| 忘了减 1 | ★ ≡ 正解 + 1 | ★ 300 / 300 | 300 / 300 |
· 忘了减 1:它恒等于正解 + 1 ⇒ 任何一组数据都错,永远只差 1。
⇒ 于是它必然被官方样例挡住(Total=4 vs Total=3)——
又一次「样例挡住的都是『每组都错』的」。
· 内层正序:它一次都没逃掉,而这也能一句话说清 ——
只要至少有一枚砝码,把最小的那种当成无限枚就能一路加到 1000,
于是它必然多称出一大片重量。
⇒ 它唯一的逃法是「一枚砝码都没有」:实测输入 0 0 0 0 0 0 时两版都是 Total=0。
★ 而那正好也是「忘了减 1」露得最明显的一组(它打出 Total=1 —— 凭空多出一种重量)。
4★ 第三个错法根本不在算法里:`Total=N` 的等号两边没有空格
// P2347 错法三:算得全对,输出格式写成了 `Total = N`//// 题面写的是 `Total=N` —— **等号两边没有空格**。// 而「Total = 3」看着更顺眼,很多人就顺手加了两个空格。//// ★★ 这一版的意义是提醒一件事:**对拍比对的时候不要 strip、不要按空白切词** ——// [第 10 章 P1068 那天踩过](/sol/p1068/):自己那一轮 300 轮全绿,// 而逐字节比报了 307 轮不一致。// ⇒ 这一类错**只有逐字节比才抓得到**,而它在评测机上是实打实的 0 分。
#include <bits/stdc++.h>using namespace std;
int main() { const int W[6] = { 1, 2, 3, 5, 10, 20 }; const int MAXW = 1000; vector<char> f(MAXW + 1, 0); f[0] = 1;
for (int i = 0; i < 6; i++) { int a; if (!(cin >> a)) return 0; for (int k = 1; a > 0; k <<= 1) { int take = min(k, a); a -= take; int cw = W[i] * take; if (cw > MAXW) continue; for (int j = MAXW; j >= cw; j--) if (f[j - cw]) f[j] = 1; } }
int cnt = 0; for (int j = 1; j <= MAXW; j++) cnt += f[j]; printf("Total = %d\n", cnt); // ⚠ 多了两个空格 return 0;}点「运行 ▶」看结果
算得一个字都不差,输出 Total = 3 —— 在评测机上是实打实的 0 分。
第 10 章 P1068 那天踩过:自己那一轮对拍 300 轮全绿,
而 check:viz 的逐字节比报了 307 轮不一致 —— 差别就在这种空白字符上。
⇒ 对拍比对不要 .strip()、不要按空白切词。
本页的对拍表里就挂着这一版,靠的正是逐字节比。
5★★★ 题面那句「其总重 ≤ 1000」—— 顺手写的生成器只有 6% 满足它
题面在第一句话里就写着「其总重 ≤ 1000」。这是一条对输入的保证, 而生成器最自然的写法是「六个数各自随机」:
a[i] = rand() % 101 // 各自在 [0, 100] 里随机
算一下这样造出来的期望总重:50 × (1 + 2 + 3 + 5 + 10 + 20) = 50 × 41 = 2050。
| 「六个数各自随机 [0,100]」,300 轮 | |
|---|---|
| 平均总重(实测) | 2047(算出来的期望 2050 —— 对上了) |
| 满足「总重 ≤ 1000」的轮数 | ★ 19 / 300(6.3%) |
| 而照题面造的默认档 | 300 / 300 |
这和第 13 章 P1162 是同一件事 —— 那道题随机 01 方阵只有 10.8% 是合法输入,照题面顶格随机更是精确的 0。
⇒ 不照着保证造,测的就是题目根本不会给的输入: 一方面白跑(那些轮次的结论对评测没意义), 另一方面还会把真正该测的形状挤掉(这道题里就是「总重接近 1000」那一档)。
★ 所以这一页的生成器是「先抽一个目标总重,再一枚一枚地随机加砝码」—— 它天生满足题面,而且能均匀地覆盖 0 到 1000 的总重。
6度量程序和生成器
7一页纸
| 关键的一步 | 布尔版多重背包:max 换成「或」,f[j] |= f[j − cw] |
| 哪一版能 AC | p2347.cpp —— 二进制拆分;⚠ 而这道题拆不拆都能过 |
| ★★ 题单那句话验了 | 摊开 ≡ 拆分 ≡ bitset,300 组不一致 0 组(拆完答案真的没变) |
| 拆分省了多少 | 顶格(1000 枚 1g):100 万次 → 9010 次,110 倍 —— 但两边都够快 |
| 错法一 | 内层正序 ⇒ ≡「六种都无限枚」(300/300);300/300 被抓,唯一逃法是输入全 0 |
| 错法二 | 忘了减 1 ⇒ ★ ≡ 正解 + 1(300/300);每组都错 ⇒ 必被样例挡住 |
| 错法三 | 输出写成 Total = N —— ⚠ 只有逐字节比才抓得到(别 strip) |
| ★★★ 生成器 | 「六个数各自随机」只有 19/300(6.3%) 满足题面的「总重 ≤ 1000」 (平均总重 2047,算出来的期望 2050)⇒ 看到「保证……」先算一遍命中率 |