题单 · 习题解析

洛谷 P1055 [NOIP 2008 普及组] ISBN 号码

★★★ 这道题的难度全部集中在题面的一句话上:「**如果余数为 10,则识别码为大写字母 X**」—— 它是整道题唯一一处不对称,而 `s[12] - '0'` 在 `X` 上是 **40**、`'0' + 10` 打出来是 `:`,**两处会一起错**;★★★ 「多久遇到一次 X」不用估,一遍 DP 就精确算得出来:10⁹ 个九位串里余数为 10 的有 **90 909 090** 个 = **9.0909%**(⚠ 而分布**不完全平**:`10⁹ = 11 × 90909090 + 10`,多出来的 10 个分给了别的余数 ⇒ **X 恰好落在少的那一边**),而这个算出来的数**配了自检**(n ≤ 6 时和真正的暴力枚举逐档核对,6 档全对);★★★ 十二格「触发 ≡ 抓获」一个不差,三条触发条件各是一句话;★★★ 而那处不对称被**三样东西同时藏了起来**:官方两组样例用的是同一组九位数字(余数都是 4,两组都碰不到 X)/ 顺手写的生成器十轮才撞上一次(31 / 300,和 9.09% 对得上)/ 题面前面反复说「识别码是数字」,那句 X 在第三段末尾;★★ 而专门为 X 造的那一档把「只打一位识别码」打成**能证的精确的 0**(那一档答案恒是 `Right`)⇒ [第 7 章 P1638](/sol/p1638/)「为一个 bug 精心造的档正是另一个的盲区」的又一次;★ 顺带一笔:加权和最大 405 ⇒ `int` 余量 530 万倍,这道题不用想类型

原题:洛谷 P1055出自 第 47 章 字符串基础:读进来、切开、比对 的题单题面本地存档:2026-09-07
⚠ 先自己写一遍,再往下看

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

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 = 158158 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) = 405int 余量 530 万倍,这道题不用想类型。

p1055.cpp★ 正解:跳过分隔符 → 加权求和 → mod 11
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

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%

p1055X.cpp✗ 错法一:把识别码当成一个数字
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3⚠ 另外两个错法:一个把权重算错,一个把格式打错

✗ 错法二:拿下标当权重 —— 没跳过分隔符

顺手写成「从左到右扫,第 i 个字符的权重就是 i+1」:三个分隔符占掉了 1、5、11 三个位置, 九位数字于是拿到 1 / 3 4 5 / 7 8 9 10 11,而题面要的是 1..9

★ 触发条件:两套权重算出的余数不同。⚠ 它不是「每一组都错」型 —— 两个余数会撞上(大约 11 分之一),下面那张表里它是 273 / 300,不是 300。 ⚠ 而它在官方样例二上打出的是 Right —— 一个错得很安静的「对」。

p1055Idx.cpp✗ 错法二:权重用了字符下标
✗ 错法三:算对了,只打出了那一位识别码

题面写着「按照规定的格式,输出正确的 ISBN 号码(包括分隔符 -)」。 只打一位是这道题最常见的「算法全对、格式全错」。 ★ 触发条件和数字完全无关:答案不是 Right

p1055Out.cpp✗ 错法三:只打一位

4★★★ 「多久遇到一次 X」不用估,能精确算 —— 而算它的代码配了自检

p1055Count.cpp★ DP 精确算出 10⁹ 个九位串的余数分布,外加一次自检
// 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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 9.0909%,以及那个差 1 的细节

九位数字一共 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★★★ 对拍:十二格「触发 ≡ 抓获」一个不差

p1055Gen.cpp★ 生成器:档 2 专门造「正确识别码是 X」
// 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
★★★ 三条读得出来的结论
  1. ★★★ 十二格全是「触发 ≡ 抓获」,一个不差,三条触发条件各是一句话: 「正确识别码是 X」/「两套权重的余数不同」/「答案不是 Right」。

  2. ★★ 档 0 那个 31 和上一步算出来的 9.0909% 对得上(期望 27.3,实测 31)—— ⇒ 顺手写的生成器不是抓不到它,是十轮里才碰上一次 ⚠ 而这恰恰是最难受的抓获率: 跑 20 轮全绿,你会以为它是对的。

  3. ★★★ 档 2 把「只打一位」打成了精确的 0,而这个 0 能证 —— 那一档的识别码写的就是正确的那个 ⇒ 答案恒是 Right ⇒ 输出格式那条路一次都没走到。 ⇒ 第 7 章 P1638 那条的又一次:为一个 bug 精心造的档位,正是另一个 bug 的盲区。

6★★ 官方样例:把两个错法打死,唯独放过那个唯一的不对称

★★★ 三样东西一起把 X 藏了起来
✗ 忘了 X ✗ 权重用下标 ✗ 只打一位
样例一 0-670-82162-4Right 放过 放过
样例二 0-670-82162-0 → 整串 放过 (打出 Right (打出 4

⚠⚠ 两组样例用的是同一组九位数字,余数都是 4 ⇒ 它们结构上碰不到 X 那一档

⇒ ★★★ 于是这道题最容易漏的那一处,被三样东西同时藏了起来

  1. 官方样例:两组的余数都是 4;
  2. 顺手写的生成器:识别码只取 0~9,而且 10 轮才撞上一次(31 / 300);
  3. 先入为主的读法:题面前面反复说「识别码是数字」,那句 X 在第三段的末尾。

⇒ ★★ 这是「这组样例在结构上问不出这个问题」最省事的一次自查: 题面里凡是出现「如果……则……」这种打破规律的话,先去看一眼样例覆盖了没有。

7★ 哪一版就已经能过了

★ 这道题只有一版,写对就是终点
版本 结果 说明
p1055.cpp AC 13 个字符、一个循环
✗ 忘了 X WA 两组样例全放过,9% 的输入上错
✗ 权重用下标 WA 样例一就死
✗ 只打一位 WA 样例二就死

⇒ ★★ 一句话带走:这道题的难度全部集中在题面的一句话上 —— 「如果余数为 10,则识别码为大写字母 X」。 而围绕它能问出的三件事,这一页各回答了一遍: 它多久发生一次(9.0909%,精确算的)/ 样例覆盖它了吗(没有)/ 为它专门造的数据会不会遮住别的(会 —— 「只打一位」在那一档是精确的 0)。