0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1055,日期见页头。两边不一致时信原站。
题目描述
每一本正式出版的图书都有一个 ISBN 号码与之对应,ISBN 码包括 9 位数字、1 位识别码和 3 位分隔符,
其规定格式如 x-xxx-xxxxx-x,其中符号 - 就是分隔符(键盘上的减号),最后一位是识别码,
例如 0-670-82162-4 就是一个标准的 ISBN 码。ISBN 码的首位数字表示书籍的出版语言,
例如 0 代表英语;第一个分隔符 - 之后的三位数字代表出版社,例如 670 代表维京出版社;
第二个分隔符后的五位数字代表该书在该出版社的编号;最后一位为识别码。
识别码的计算方法如下:
首位数字乘以 1 加上次位数字乘以 2 ……以此类推,用所得的结果 mod 11,所得的余数即为识别码,
如果余数为 10,则识别码为大写字母 X。例如 ISBN 号码 0-670-82162-4 中的识别码 4 是这样得到的:
对 067082162 这 9 个数字,从左至右,分别乘以 1, 2, …, 9 再求和,即 0×1 + 6×2 + …… + 2×9 = 158,
然后取 158 mod 11 的结果 4 作为识别码。
你的任务是编写程序判断输入的 ISBN 号码中识别码是否正确,如果正确,则仅输出 Right;
如果错误,则输出你认为是正确的 ISBN 号码。
输入格式
一个字符序列,表示一本书的 ISBN 号码(保证输入符合 ISBN 号码的格式要求)。
输出格式
一行,假如输入的 ISBN 号码的识别码正确,那么输出 Right,否则,按照规定的格式,
输出正确的 ISBN 号码(包括分隔符 -)。
数据范围
2008 普及组第一题。时限 1 秒,内存 131072 KB(128 MB)。
输入输出样例
输入
0-670-82162-4
输出
Right
0×1 + 6×2 + 7×3 + 0×4 + 8×5 + 2×6 + 1×7 + 6×8 + 2×9 = 158,158 mod 11 = 4 ⇒ 和末位的 4 一致。
输入
0-670-82162-0
输出
0-670-82162-4
★ 同样的九位数字,末位写成了 0 ⇒ 输出整串正确的号码,分隔符也要打出来。
1★ 一句话:跳过分隔符,加权求和,mod 11
x-xxx-xxxxx-x 一共 13 个字符,而且题面明写「保证输入符合 ISBN 号码的格式要求」
⇒ 不用做任何格式校验,九位数字的下标是固定的 0 / 2 3 4 / 6 7 8 9 10,识别码在 12 号位。
⚠ 但必须跳过那三个 - —— 权重是「第几个数字」,不是「第几个字符」(第 ③ 步那个错法)。
★ 一笔一眼的账:加权和最大 9 × (1+2+…+9) = 405 ⇒ int 余量 530 万倍,这道题不用想类型。
// P1055 ISBN 号码 —— 正解:跳过分隔符,加权求和,mod 11//// ★ 题面把格式钉死了(`x-xxx-xxxxx-x`,而且「保证输入符合格式要求」)// ⇒ 九位数字的下标是固定的:0 / 2 3 4 / 6 7 8 9 10,识别码在 12 号位。// ⚠ 所以**不用做格式校验**,但**必须跳过那三个 `-`**(见 p1055Idx.cpp:拿下标当权重的那一版)。//// ⚠ 这道题只有一处不对称,而它就是全部的坑:**余数 10 要写成大写字母 `X`**。// ⇒ 于是「识别码」不是一个数字,是一个**字符**(见 p1055X.cpp)。// ★ 一笔一眼的账:加权和最大 9 × (1+2+…+9) = 405 ⇒ `int` 余量大到不用想。#include <bits/stdc++.h>using namespace std;
int main() { string s; if (!(cin >> s)) return 0; int sum = 0, w = 1; for (int i = 0; i < 12; i++) if (s[i] != '-') sum += (s[i] - '0') * w++; // ★ 跳过分隔符,权重才是 1..9 int r = sum % 11; char want = (r == 10) ? 'X' : (char)('0' + r); // ⚠ 这道题唯一的不对称 if (s[12] == want) printf("Right\n"); else { s[12] = want; printf("%s\n", s.c_str()); } // ⚠ 要打整串,带分隔符 return 0;}点「运行 ▶」看结果
2★★★ 这道题只有一处不对称,而它就是全部的坑
题面里唯一一句打破规律的话是:「如果余数为 10,则识别码为大写字母 X」。
⇒ 一句话的后果有两处,而且两处会一起错:
if (s[12] - '0' == r) ... // ⚠ 'X' - '0' 是 40,永远不等于任何余数
s[12] = (char)('0' + r); // ⚠ r = 10 时打出 ':'(ASCII 58)★ 触发条件写成一句话:正确的识别码正好是 X,也就是加权和 mod 11 == 10。
⇒ 下一步会把这个「多久发生一次」精确算出来:9.0909%。
// P1055 ✗ 错法一:把识别码当成一个数字 —— 忘了余数 10 写作 `X`//// ⚠ 两处一起错,而且是同一个原因:// ① 比较时写 `s[12] - '0'` —— 而 `'X' - '0'` 是 **40**,永远不等于任何余数;// ② 输出时写 `'0' + r` —— `r = 10` 时打出来的是 `:`(ASCII 58),一个不该存在的字符。// ★ 触发条件是一句话:**正确的识别码正好是 `X`(也就是加权和 mod 11 == 10)**。// ⇒ 而随机一个 ISBN 落到这一档的比例是 **9.09%**(p1055Count.cpp 里那张表是精确算出来的)// ⚠ 官方两组样例的余数都是 4 ⇒ **两组都碰不到它**。#include <bits/stdc++.h>using namespace std;
int main() { string s; if (!(cin >> s)) return 0; int sum = 0, w = 1; for (int i = 0; i < 12; i++) if (s[i] != '-') sum += (s[i] - '0') * w++; int r = sum % 11; if (s[12] - '0' == r) printf("Right\n"); // ⚠ 'X' - '0' = 40 else { s[12] = (char)('0' + r); printf("%s\n", s.c_str()); } // ⚠ r = 10 时打出 ':' return 0;}点「运行 ▶」看结果
3⚠ 另外两个错法:一个把权重算错,一个把格式打错
顺手写成「从左到右扫,第 i 个字符的权重就是 i+1」:三个分隔符占掉了 1、5、11 三个位置,
九位数字于是拿到 1 / 3 4 5 / 7 8 9 10 11,而题面要的是 1..9。
★ 触发条件:两套权重算出的余数不同。⚠ 它不是「每一组都错」型 ——
两个余数会撞上(大约 11 分之一),下面那张表里它是 273 / 300,不是 300。
⚠ 而它在官方样例二上打出的是 Right —— 一个错得很安静的「对」。
题面写着「按照规定的格式,输出正确的 ISBN 号码(包括分隔符 -)」。
只打一位是这道题最常见的「算法全对、格式全错」。
★ 触发条件和数字完全无关:答案不是 Right。
4★★★ 「多久遇到一次 X」不用估,能精确算 —— 而算它的代码配了自检
// P1055 解析页上所有数字的出处。// ./p1055Count 人话版// ./p1055Count csv 给 check:viz 用//// ★★ 这道题只有一处不对称(余数 10 写作 `X`),而这一处正是最容易漏的。// ⇒ 于是最该问的一句是:**随机一个 ISBN,识别码是 `X` 的概率有多大?**// 它决定了「忘了 X」那个错法在对拍里多久现形一次。//// ⚠ 这个数**不用跑对拍去估,能精确算**:九位数字的加权和 mod 11 的分布,一遍 DP 就出来了// (10⁹ 个九位串,逐个试太慢;而 DP 是精确的)。// ★ 而 DP 本身**配了自检**:位数 n ≤ 6 时拿真正的暴力枚举(最多 10⁶ 个)逐档核对// ⇒ [第 19 章 P2240](/sol/p2240/) 那条规矩:报一个算出来的数之前,先证明算它的代码是活的。#include <bits/stdc++.h>using namespace std;
/** 前 n 位(权重 1..n)的加权和 mod 11 的分布 —— DP */static array<long long, 11> dist(int n) { array<long long, 11> f{}; f[0] = 1; for (int i = 1; i <= n; i++) { array<long long, 11> g{}; for (int r = 0; r < 11; r++) { if (!f[r]) continue; for (int d = 0; d <= 9; d++) g[(r + d * i) % 11] += f[r]; } f = g; } return f;}
/** 同一件事的暴力版(只对小 n 用,给 DP 做自检) */static array<long long, 11> brute(int n) { array<long long, 11> f{}; long long total = 1; for (int i = 0; i < n; i++) total *= 10; for (long long x = 0; x < total; x++) { long long v = x; int sum = 0; for (int i = n; i >= 1; i--) { sum += (int)(v % 10) * i; v /= 10; } f[sum % 11]++; } return f;}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
/* ① 自检:n = 1..6 上 DP 和暴力逐档比 */ int mismatch = 0; for (int n = 1; n <= 6; n++) if (dist(n) != brute(n)) mismatch++;
/* ② 九位的真实分布 */ auto f9 = dist(9); long long total = 0; for (int r = 0; r < 11; r++) total += f9[r]; double pctX = 100.0 * (double)f9[10] / (double)total;
/* ③ 官方两组样例的余数 */ auto rOf = [](const string& s) { int sum = 0, w = 1; for (int i = 0; i < 12; i++) if (s[i] != '-') sum += (s[i] - '0') * w++; return sum % 11; }; int r1 = rOf("0-670-82162-4"), r2 = rOf("0-670-82162-0");
/* ④ 加权和的上界 */ int maxSum = 0; for (int i = 1; i <= 9; i++) maxSum += 9 * i;
if (csv) { printf("mismatch,%d\n", mismatch); printf("total,%lld\ncountX,%lld\npctX,%.4f\n", total, f9[10], pctX); printf("minR,%lld\nmaxR,%lld\n", *min_element(f9.begin(), f9.end()), *max_element(f9.begin(), f9.end())); printf("sample1,%d\nsample2,%d\n", r1, r2); printf("maxSum,%d\n", maxSum); return 0; }
printf("① 自检:n = 1..6 上 DP 和暴力枚举对不上的档数 = %d\n", mismatch); printf("② 九位数字一共 %lld 个,加权和 mod 11 == 10(识别码是 X)的有 %lld 个 = %.4f%%\n", total, f9[10], pctX); printf(" (11 个余数里最少的 %lld 个、最多的 %lld 个 —— 分布几乎是平的,但不完全平)\n", *min_element(f9.begin(), f9.end()), *max_element(f9.begin(), f9.end())); printf("③ 官方两组样例的余数都是 %d / %d ⇒ 两组都碰不到 X 那一档\n", r1, r2); printf("④ 加权和最大 %d ⇒ int 余量 %.0f 倍,这道题不用想类型\n", maxSum, 2147483647.0 / maxSum); return 0;}点「运行 ▶」看结果
九位数字一共 10⁹ 种,加权和 mod 11 的分布用一遍 DP 就出来了(逐个试太慢,而 DP 是精确的):
余数 == 10(识别码是 X)的九位串 |
90 909 090 个 |
| 占比 | ★ 9.0909% |
| 11 个余数里最少的 / 最多的 | 90 909 090 / 90 909 091 |
★ 分布几乎是平的,可不完全平:10⁹ = 11 × 90 909 090 + 10,多出来的 10 个分给了 10 个余数
⇒ 恰好是 X 那一档落在少的那一边(差 1 个)。
⚠⚠ 而这个「精确算出来的数」配了自检才敢写:位数 n ≤ 6 时拿真正的暴力枚举
(最多 10⁶ 个串)逐档核对 —— 6 档全部对上,0 档不同。
⇒ 第 19 章 P2240 立的那条规矩:报一个算出来的数之前,先证明算它的代码是活的。
5★★★ 对拍:十二格「触发 ≡ 抓获」一个不差
// P1055 的生成器:./p1055Gen 种子 [档位]//// ★ 三个错法各靠什么现形:// · p1055X(忘了余数 10 写作 X)→ **正确的识别码正好是 X**(加权和 mod 11 == 10);// · p1055Idx(拿下标当权重) → **两套权重算出的余数不同**;// · p1055Out(只打一位识别码) → **答案不是 Right**。//// 档位:// 0 ★ 顺手写的:九位随机数字 + 识别码也随机取 0~9(⚠ 它从不产生「输入里就是 X」这种串)// 1 ⚠ 一半的轮次保证识别码是对的 ⇒ 「Right」和「要改」各占一半// 2 ⚠ 专门档:**保证正确的识别码是 X**(p1055X 的专门档)// 3 ★ 最终档:九位随机、识别码一半对一半随机(含 X)//// ⚠ rng() 一律先落到具名变量再传参([第 24 章 P1776](/sol/p1776/) 那一跤)。#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1; int mode = argc > 2 ? atoi(argv[2]) : 0; mt19937 rng(seed * 1000003u + 20260907u);
int d[9]; for (;;) { for (int i = 0; i < 9; i++) { unsigned r = rng() % 10u; d[i] = (int)r; } int sum = 0; for (int i = 0; i < 9; i++) sum += d[i] * (i + 1); if (mode == 2 && sum % 11 != 10) continue; // ⚠ 专门档:只要余数正好是 10 的 break; } int sum = 0; for (int i = 0; i < 9; i++) sum += d[i] * (i + 1); int r = sum % 11; char right = (r == 10) ? 'X' : (char)('0' + r);
char last; if (mode == 2) last = right; // 专门档:识别码就写对的那个(含 X) else if (mode == 1) { unsigned f = rng() % 2u; if (f) last = right; else { unsigned g = rng() % 10u; last = (char)('0' + g); } } else if (mode == 3) { unsigned f = rng() % 2u; if (f) last = right; else { unsigned g = rng() % 11u; last = (g == 10) ? 'X' : (char)('0' + g); } } else { unsigned g = rng() % 10u; last = (char)('0' + g); } // ★ 顺手写的:只会产生数字
printf("%d-%d%d%d-%d%d%d%d%d-%c\n", d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], last); return 0;}点「运行 ▶」看结果
四档 × 300 轮:
| 档位 | ✗ 忘了 X |
✗ 权重用下标 | ✗ 只打一位 |
|---|---|---|---|
| 0 ★ 顺手写的(识别码随机取 0~9) | 31 | 273 | 272 |
| 1 ⚠ 一半保证识别码正确 | 31 | 273 | 151 |
2 ⚠ 专门造「正确识别码是 X」 |
★ 300 | 276 | ★ 0 |
| 3 ★ 最终档 | 31 | 273 | 150 |
-
★★★ 十二格全是「触发 ≡ 抓获」,一个不差,三条触发条件各是一句话: 「正确识别码是
X」/「两套权重的余数不同」/「答案不是Right」。 -
★★ 档 0 那个 31 和上一步算出来的 9.0909% 对得上(期望 27.3,实测 31)—— ⇒ 顺手写的生成器不是抓不到它,是十轮里才碰上一次 ⚠ 而这恰恰是最难受的抓获率: 跑 20 轮全绿,你会以为它是对的。
-
★★★ 档 2 把「只打一位」打成了精确的 0,而这个 0 能证 —— 那一档的识别码写的就是正确的那个 ⇒ 答案恒是
Right⇒ 输出格式那条路一次都没走到。 ⇒ 第 7 章 P1638 那条的又一次:为一个 bug 精心造的档位,正是另一个 bug 的盲区。
6★★ 官方样例:把两个错法打死,唯独放过那个唯一的不对称
✗ 忘了 X |
✗ 权重用下标 | ✗ 只打一位 | |
|---|---|---|---|
样例一 0-670-82162-4 → Right |
放过 | ★ 死 | 放过 |
样例二 0-670-82162-0 → 整串 |
放过 | ★ 死(打出 Right) |
★ 死(打出 4) |
⚠⚠ 两组样例用的是同一组九位数字,余数都是 4 ⇒ 它们结构上碰不到 X 那一档。
⇒ ★★★ 于是这道题最容易漏的那一处,被三样东西同时藏了起来:
- 官方样例:两组的余数都是 4;
- 顺手写的生成器:识别码只取 0~9,而且 10 轮才撞上一次(31 / 300);
- ★ 先入为主的读法:题面前面反复说「识别码是数字」,那句
X在第三段的末尾。
⇒ ★★ 这是「这组样例在结构上问不出这个问题」最省事的一次自查: 题面里凡是出现「如果……则……」这种打破规律的话,先去看一眼样例覆盖了没有。
7★ 哪一版就已经能过了
| 版本 | 结果 | 说明 |
|---|---|---|
★ p1055.cpp |
AC | 13 个字符、一个循环 |
✗ 忘了 X |
WA | ⚠ 两组样例全放过,9% 的输入上错 |
| ✗ 权重用下标 | WA | 样例一就死 |
| ✗ 只打一位 | WA | 样例二就死 |
⇒ ★★ 一句话带走:这道题的难度全部集中在题面的一句话上 ——
「如果余数为 10,则识别码为大写字母 X」。
而围绕它能问出的三件事,这一页各回答了一遍:
它多久发生一次(9.0909%,精确算的)/ 样例覆盖它了吗(没有)/
为它专门造的数据会不会遮住别的(会 —— 「只打一位」在那一档是精确的 0)。