0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1071,日期见页头。两边不一致时信原站。
题目描述
R 国和 S 国正陷入战火之中,双方都互派间谍,潜入对方内部,伺机行动。历尽艰险后, 潜伏于 S 国的 R 国间谍小 C 终于摸清了 S 国军用密码的编码规则:
- S 国军方内部欲发送的原信息经过加密后在网络上发送,原信息的内容与加密后所得的内容
均由大写字母
A~Z构成(无空格等其他字符); - S 国对于每个字母规定了对应的密字。加密的过程就是将原信息中的所有字母替换为其对应的密字;
- 每个字母只对应一个唯一的密字,不同的字母对应不同的密字。 密字可以和原字母相同。
小 C 的破译过程是这样的:扫描原信息,对于原信息中的字母 x,找到其在加密信息中的对应大写字母 y,
并认为在密码里 y 是 x 的密字。如此进行下去直到停止于如下的某个状态:
- 所有信息扫描完毕,
A~Z所有 26 个字母在原信息中均出现过并获得了相应的密字; - 所有信息扫描完毕,但发现存在某个(或某些)字母在原信息中没有出现;
- 扫描中发现掌握的信息里有明显的自相矛盾或错误(违反 S 国密码的编码规则)。
现在请你帮助小 C:通过内线掌握的信息,尝试破译密码。然后利用破译的密码,翻译电报中的加密信息。
输入格式
共三行,每行为一个长度在 1 到 100 之间的字符串。
第一行,为小 C 掌握的一条加密信息;第二行,为第一行的加密信息所对应的原信息; 第三行,为 R 国司令部要求小 C 翻译的加密信息。
输入数据保证所有字符串仅由大写字母 A~Z 构成,且第一行长度与第二行相等。
输出格式
共一行。若破译密码停止时出现 2、3 两种情况,请你输出 Failed;
否则请输出利用密码翻译电报中加密信息后得到的原信息。
数据范围
NOIP2009 提高组第一题。时限 1 秒,内存 128000 KB(125 MB)。
输入输出样例
输入
AA AB EOWIE
输出
Failed
原信息中的字母 A 和 B 对应相同的密字 ⇒ 违反第 3 条规则,输出 Failed。
输入
QWERTYUIOPLKJHGFDSAZXCVBN ABCDEFGHIJKLMNOPQRSTUVWXY DSLIEWO
输出
Failed
字母 Z 在原信息中没有出现 ⇒ 停在第 2 种状态,输出 Failed。
输入
MSRTZCJKPFLQYVAWBINXUEDGHOOILSMIJFRCOPPQCEUNYDUMPP YIZSDWAHLNOVFUCERKJXQMGTBPPKOIYKANZWPLLVWMQJFGQYLL FLSO
输出
NOIP
★ 正常破译出来的那一组。
1★ 一句话:建一张双向的字母表,三条规则一条都不能漏
| 题面 | 要检查什么 |
|---|---|
| 「每个字母只对应一个唯一的密字」 | 同一个原文字母 x 只能有一个密字 y |
| ★「不同的字母对应不同的密字」 | 同一个密字 y 只能来自一个原文字母 x |
| 「26 个字母均出现过」 | A~Z 每一个都得在原信息里出现 |
⇒ ★★ 前两条是同一件事的两个方向(这张表要是个一一对应),漏掉哪个方向都会放过一整类输入; 第三条和前两条毫无关系,是另一件必须单独写的检查。
⚠ 读入用 cin >> a >> b >> c 读三个「词」就好 —— 官方样例的输入里真的带着尾随空格,
getline 反而要自己去 trim。
// P1071 潜伏者 —— 正解:建一张双向的字母表,三种「无解」一个都不能漏//// ★ 题面把编码规则写成了三条,而**每一条都对应一件要检查的事**:// ② 加密就是把原信息里的字母换成它的密字 -> 同一个原文字母 x 只能有一个密字 y;// ③ 不同的字母对应不同的密字 -> 同一个密字 y 只能来自一个原文字母 x;// ①「A~Z 全部出现过」才算破译完成 -> 26 个字母**都要**在原信息里出现过。// ⇒ ★★ 前两条是**双向**的,漏掉哪一个方向都会放过一整类输入(见 p1071One.cpp);// 第三条则和前两条完全无关,是另一件事(见 p1071Cover.cpp)。//// ⚠ 输入用 `cin >> a >> b >> c` 读三个「词」,这样行末的空格和换行怎么摆都无所谓// —— 官方样例的输入里**真的带着尾随空格**。#include <bits/stdc++.h>using namespace std;
int main() { string a, b, c; if (!(cin >> a >> b >> c)) return 0; if (a.size() != b.size()) { printf("Failed\n"); return 0; }
int enc[26], dec[26]; memset(enc, -1, sizeof(enc)); memset(dec, -1, sizeof(dec)); for (size_t i = 0; i < b.size(); i++) { int x = b[i] - 'A'; // 原文字母 int y = a[i] - 'A'; // 它的密字 if (enc[x] != -1 && enc[x] != y) { printf("Failed\n"); return 0; } // 一个 x 两个 y if (dec[y] != -1 && dec[y] != x) { printf("Failed\n"); return 0; } // 一个 y 两个 x enc[x] = y; dec[y] = x; } for (int x = 0; x < 26; x++) if (enc[x] == -1) { printf("Failed\n"); return 0; } // 有字母没出现过
string out; for (char ch : c) out.push_back((char)('A' + dec[ch - 'A'])); printf("%s\n", out.c_str()); return 0;}点「运行 ▶」看结果
2⚠ 三个错法,各拆掉三条规则里的一条
if (enc[x] != -1 && enc[x] != y) Failed; // ✓ 一个 x 只能有一个 y
// ⚠ 少了这一句:if (dec[y] != -1 && dec[y] != x) Failed;
enc[x] = y; dec[y] = x;★ 触发条件要三件事同时成立:enc 方向自洽 + dec 方向有冲突 + 26 个字母全出现过。
⇒ 下面会看到,这个形状顺手写的生成器一辈子造不出来。
// P1071 ✗ 错法一:只查了一个方向//// ⚠ 题面第 ③ 条是**两句话**:「每个字母只对应一个唯一的密字」+「**不同的字母对应不同的密字**」。// 只写前一句(`enc[x]` 一致)就漏掉了后一句 —— 两个不同的原文字母**可以**被映到同一个密字。// ★ 官方**样例一**考的正是这个:密文 `AA` ↔ 原文 `AB` ⇒ A 和 B 的密字都是 A。// ★ 触发条件:**enc 方向自洽、可 dec 方向有冲突、而且 26 个字母都出现过**。#include <bits/stdc++.h>using namespace std;
int main() { string a, b, c; if (!(cin >> a >> b >> c)) return 0; if (a.size() != b.size()) { printf("Failed\n"); return 0; }
int enc[26], dec[26]; memset(enc, -1, sizeof(enc)); memset(dec, -1, sizeof(dec)); for (size_t i = 0; i < b.size(); i++) { int x = b[i] - 'A', y = a[i] - 'A'; if (enc[x] != -1 && enc[x] != y) { printf("Failed\n"); return 0; } enc[x] = y; dec[y] = x; // ⚠ 少了「一个 y 只能来自一个 x」那一句 } for (int x = 0; x < 26; x++) if (enc[x] == -1) { printf("Failed\n"); return 0; }
string out; for (char ch : c) out.push_back((char)('A' + dec[ch - 'A'])); printf("%s\n", out.c_str()); return 0;}点「运行 ▶」看结果
★ 触发条件:没有任何冲突,但 26 个字母没出全。官方样例二考的正是这个(原信息里没有 Z)。
⚠ 而它错得很难看:没破译出来的那些位置只能瞎打一个字符 ——
屏幕上几乎看不出来,评测机只会给你一个 WA。
题面的输入顺序是「第一行加密信息、第二行原信息」—— 和直觉正好相反
(大多数人会以为「先给原文,再给密文」)。
★ 触发条件:把两行交换之后,答案和原来不一样。
⚠ 注意这不等于「一定错」:交换之后仍然可能双双 Failed,那就看不出来了。
3★★★ 这一页真正的功课:照题面随机造数据,300 轮全是 Failed
// P1071 解析页上那几个「生成器为什么必须反着造」的数字的出处。// ./p1071Count 人话版// ./p1071Count csv 给 check:viz 用//// ★★ 它只回答一个问题,而这个问题决定了整页的对拍策略:// **随机造一段长度 L 的原文,26 个字母全出现的概率有多大?**// —— 这是「[生成器该照抄题面的比值](/sol/p1020/)」在这道题上的具体形态:// 题面允许 L 小到 1、大到 100,而顺手写的生成器在 `1..100` 里均匀摇 L// ⇒ 绝大多数轮次连 26 个字母都凑不齐,正解直接 Failed,**对拍在验零**。//// ⚠ 这个概率用容斥算是精确的(不用估、不用跑蒙特卡洛):// P(全覆盖) = Σ_{k=0..26} (-1)^k C(26,k) ((26-k)/26)^L#include <bits/stdc++.h>using namespace std;
static double coverProb(int L) { double p = 0; for (int k = 0; k <= 26; k++) { double term = 1; for (int i = 0; i < k; i++) term = term * (26 - i) / (i + 1); // C(26,k) term *= pow((26.0 - k) / 26.0, (double)L); p += (k % 2 ? -term : term); } return p;}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
/* 顺手写的生成器:L 在 1..100 里均匀摇 ⇒ 平均覆盖概率 */ double avg = 0; for (int L = 1; L <= 100; L++) avg += coverProb(L); avg /= 100.0;
/* 期望要多长才凑得齐 26 个字母(优惠券收集问题)*/ double need = 0; for (int i = 1; i <= 26; i++) need += 26.0 / i;
/* 最小的、覆盖概率超过一半的 L */ int half = 0; for (int L = 26; L <= 100000; L++) if (coverProb(L) >= 0.5) { half = L; break; }
if (csv) { printf("cover100,%.4f\n", coverProb(100) * 100.0); printf("cover50,%.4f\n", coverProb(50) * 100.0); printf("avg,%.4f\n", avg * 100.0); printf("need,%.2f\n", need); printf("half,%d\n", half); return 0; } printf("① 随机原文长度 L,26 个字母全出现的概率:\n"); for (int L : {26, 50, 75, 100}) printf(" L = %3d -> %.4f%%\n", L, coverProb(L) * 100.0); printf("② 顺手写的生成器(L 在 1..100 里均匀摇)平均覆盖概率只有 %.4f%%\n", avg * 100.0); printf("③ 期望要 %.2f 个字母才凑得齐 26 种(优惠券收集);\n", need); printf(" 而覆盖概率第一次过半,要 L = %d\n", half); return 0;}点「运行 ▶」看结果
随机一段长度 L 的原文,26 个字母全出现的概率(容斥公式,精确值):
L |
26 | 50 | 75 | ★ 100(题面顶格) |
|---|---|---|---|---|
| 全覆盖概率 | 0.0000% | 0.7745% | 21.4134% | ★ 58.2242% |
★★★ 而凑齐 26 个字母期望要 100.21 个字母(26 × H₂₆)——
题面的长度上限正好是 100。
⇒ 于是「26 个字母都出现过」这件事,在题面顶格时也只有一半多一点的机会成立;
而顺手写的生成器在 1..100 里均匀摇 L,平均覆盖概率只有 12.44%
(覆盖概率第一次过半要 L = 94)。
⚠⚠ 更糟的还在后面:随机造的两行之间几乎必然撞出冲突
⇒ 照题面随机造三行,正解 300 轮全部输出 Failed。
⇒ 三个错法多半也跟着一起 Failed,对拍记「通过」,而它验的是零
(「一致有两种:都算对了,和都没算」)。
⇒ ★★ 出路只有一条:反着造 —— 先随机一个 26 字母的置换(那就是密码本), 再随机原文,拿置换加密出第一行。这样造出来的数据天生没有冲突。 (第 13 章 P1162、第 40 章 P1888 那条的又一次。)
4★★★ 对拍:十五格「触发 ≡ 抓获」,而最深的那个坑只有一档抓得到
// P1071 的生成器:./p1071Gen 种子 [档位]//// ⚠⚠ 这道题最难写的不是正解,是生成器 —— 照题面「三行随机大写字母」造出来的数据,// **正解几乎每一轮都输出 Failed**(随机两行必然撞出冲突、26 个字母也出不全)// ⇒ 三个错法多半跟着一起 Failed,对拍在**验零**// ([「一致有两种:都算对了,和都没算」](/sol/p1746/))。// ⇒ ★★ 出路是**反着造**:先随机一个 26 字母的置换(那就是密码本),// 再随机原文,拿置换加密出第一行 —— 这样造出来的数据天生没有冲突。// ([第 13 章 P1162](/sol/p1162/)、[第 40 章 P1888](/sol/p1888/) 那条的又一次。)//// 档位:// 0 ★ 顺手写的:三行随机大写字母(⚠ 前两行等长 —— 题面保证了这一条;正解几乎恒 Failed)// 1 ⚠ 反着造:随机置换 + 随机原文 ⇒ 没有冲突,但 26 个字母**不一定**出全// 2 ⚠ 反着造 + 保证 26 个字母全出现 ⇒ 正解一定输出翻译结果// 3 ⚠⚠ 专门档:26 个字母全出现,**但故意让两个原文字母共用一个密字**// (★ 这是 p1071One「只查一个方向」唯一现形的形状)// 4 ★ 最终档:上面几档混着来//// ⚠ 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); if (mode == 4) { unsigned r = rng() % 4u; mode = (int)r; } // 最终档:随机挑一档
auto randLen = [&](int lo, int hi) { unsigned r = rng() % (unsigned)(hi - lo + 1); return lo + (int)r; };
if (mode == 0) { int len12 = randLen(1, 100); // ⚠ 题面保证「第一行长度与第二行相等」 for (int i = 0; i < 3; i++) { int len = (i < 2) ? len12 : randLen(1, 100); string s; for (int k = 0; k < len; k++) { unsigned c = rng() % 26u; s.push_back((char)('A' + c)); } printf("%s\n", s.c_str()); } return 0; }
/* 反着造:先摇一个置换当密码本 */ vector<int> perm(26); for (int i = 0; i < 26; i++) perm[i] = i; shuffle(perm.begin(), perm.end(), rng);
string plain; if (mode >= 2) { // 保证 26 个字母全出现 for (int i = 0; i < 26; i++) plain.push_back((char)('A' + i)); int extra = randLen(0, 74); for (int k = 0; k < extra; k++) { unsigned c = rng() % 26u; plain.push_back((char)('A' + c)); } shuffle(plain.begin(), plain.end(), rng); } else { int len = randLen(1, 100); for (int k = 0; k < len; k++) { unsigned c = rng() % 26u; plain.push_back((char)('A' + c)); } }
if (mode == 3) { // ⚠ 让两个原文字母共用一个密字 unsigned u = rng() % 26u, v = rng() % 25u; int x = (int)u, y = (int)v; if (y >= x) y++; perm[y] = perm[x]; }
string cipher; for (char ch : plain) cipher.push_back((char)('A' + perm[ch - 'A']));
int len3 = randLen(1, 100); string third; for (int k = 0; k < len3; k++) { unsigned c = rng() % 26u; third.push_back((char)('A' + c)); }
printf("%s\n%s\n%s\n", cipher.c_str(), plain.c_str(), third.c_str()); return 0;}点「运行 ▶」看结果
五档 × 300 轮:
| 档位 | 正解答 Failed 的轮数 |
✗ 只查一个方向 | ✗ 忘了覆盖 | ✗ 两行读反 |
|---|---|---|---|---|
| 0 ★ 顺手写的(三行随机字母) | ⚠ 300 | ★ 0 | 9 | ★ 0 |
| 1 ⚠ 反着造(置换 + 随机原文) | 275 | ★ 0 | 275 | 25 |
| 2 ⚠ 反着造 + 保证 26 字母全出现 | ★ 0 | ★ 0 | ★ 0 | 298 |
| 3 ⚠⚠ 全覆盖 + 故意让两个原文字母共用一个密字 | 300 | ★ 300 | ★ 0 | ★ 0 |
| 4 ★ 最终档(前四档混着来) | 214 | 69 | 68 | 86 |
-
★★★ 顺手写的那一档(档 0)三个错法几乎一个都抓不到,而原因是「300 轮全是 Failed」 —— 随机两行必然撞冲突、26 个字母也出不全 ⇒ 所有版本一起答
Failed。 ⇒ 这一页是「对拍在验零」最干净的一次:不是抓获率低,是这批数据根本没在问问题。 -
★★★ 「只查一个方向」那个错法,只有档 3 抓得到,而档 3 的形状要三件事同时成立 (
enc自洽 +dec冲突 + 26 字母全覆盖)。 ⚠ 顺手写的三档全是精确的 0 —— 这个形状必须照着触发条件专门造: 先摇一个置换,再故意把两个字母的密字改成同一个,同时保证原文覆盖全 26 个字母。 ⇒ 第 32 章 P1462 那条:抓不到时别加轮数,去想那条线在哪儿、然后照着它造。 -
★★ 档 2 和档 3 各自把两个错法打成能证的 0: 档 2(反着造 + 全覆盖)⇒ 没有冲突、也不缺字母 ⇒ 前两个错法检查的东西根本不会发生; 档 3(全覆盖 + 有
dec冲突)⇒ 正解在冲突那一步就Failed了 ⇒ 覆盖检查那一句没轮到执行。 ⇒ ★ 第 7 章 P1638 那条:为一个 bug 精心造的档位,正是另一个 bug 的盲区。 -
★★★ 十五格「触发 ≡ 抓获」一个不差 —— 三条触发条件各是一句能写下来的话(见第 ② 步)。 ⚠ 而写这一页时踩过一次:档 0 第一版让三行长度各自随机, 于是前两行几乎总不等长 ——那违反了题面那句「第一行长度与第二行相等」, 所有版本在长度检查那一步就一起返回了。 ⇒ 顺手写的生成器也要守题面的每一句保证,否则量到的是一批题目根本不会给的输入。
5★★★ 官方三组样例:一种终止状态一组,可它们仍然放过了最深的那个错法
| ✗ 只查一个方向 | ✗ 忘了覆盖 | ✗ 两行读反 | |
|---|---|---|---|
样例一(冲突 → Failed) |
⚠ 放过 | 放过 | 放过 |
样例二(缺字母 → Failed) |
放过 | ★ 死 | 放过 |
样例三(正常 → NOIP) |
放过 | 放过 | ★ 死 |
★ 出题人是照着三种终止状态各给了一组样例的 —— 这在本书里很少见 (隔壁 P1308 那两组各只打死一个错法)。 ⚠⚠ 可「只查一个方向」那个错法,三组样例一个都打不死,而原因干净得能一句话说完:
样例一的说明写着「
A和B对应相同的密字」,可那组数据里只出现了A、B两个字母 —— 于是「只查一个方向」的程序走到覆盖检查那一步照样输出Failed。 它答对了,但理由是错的。
⇒ ★★★ 这是「这组样例在结构上问不出这个问题」最微妙的一次:
样例确实覆盖了那条规则,可它同时也满足另一条规则的失败条件,
于是错的程序被后一条救了。
⇒ ★★ 判据也很便宜:看到一组数据同时踩中两个 Failed 分支,就该再造一组只踩一个的。
6★ 哪一版就已经能过了
| 版本 | 结果 | 说明 |
|---|---|---|
★ p1071.cpp |
AC | 两张表 + 一次覆盖检查,一共二十行 |
| ✗ 只查一个方向 | WA | ⚠ 三组样例全放过,顺手写的对拍三档是精确的 0 |
| ✗ 忘了覆盖 | WA | 样例二就死 |
| ✗ 两行读反 | WA | 样例三就死 |
⇒ ★★ 一句话带走:这道题挂在提高组第一题的位置上,考的不是算法,是「把题面的每一句话都变成一行检查」 ——
而它最值得记住的一条,是那个「反着造」的动作:
照题面随机造三行,300 轮全是 Failed;先摇一个置换再造,才算真的在测。