0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1200,日期见页头。两边不一致时信原站。
题目描述
众所周知,在每一个彗星后都有一只 UFO。这些 UFO 时常来收集地球上的忠诚支持者。 不幸的是,他们的飞碟每次出行都只能带上一组支持者。因此,他们要用一种聪明的方案 让这些小组提前知道谁会被彗星带走。他们为每个彗星起了一个名字,通过这些名字来决定 这个小组是不是被带走的那个特定的小组。
小组名和彗星名都以下列方式转换成一个数字:最终的数字就是名字中所有字母的积,
其中 A 是 1,Z 是 26。例如,USACO 小组就是 21 × 19 × 1 × 3 × 15 = 17955。
如果小组的数字 mod 47 等于彗星的数字 mod 47,你就得告诉这个小组需要准备好被带走!
写出一个程序,读入彗星名和小组名并算出用上面的方案能否将两个名字搭配起来,
如果能搭配,就输出 GO,否则输出 STAY。小组名和彗星名均是没有空格或标点的一串大写字母
(不超过 6 个字母)。
输入格式
第 1 行:一个长度为 1 到 6 的大写字母串,表示彗星的名字。
第 2 行:一个长度为 1 到 6 的大写字母串,表示队伍的名字。
输出格式
一行内输出 GO 或 STAY。
数据范围
USACO Training Section 1.1。时限 1 秒,内存 128000 KB(125 MB)。
输入输出样例
输入
COMETQ HVNGAT
输出
GO
两个名字算出来 mod 47 都是 27 ⇒ GO。
输入
ABSTAR USACO
输出
STAY
ABSTAR mod 47 = 3,USACO mod 47 = 1 ⇒ STAY。
1★ 一句话:字符本来就是整数,这道题连表都不用查
第 47 章第 3 步那句「字符存的就是它的编码」在这里直接就是答案:
A 是 1、Z 是 26 ⇒ 写 c - 'A' + 1,不用查表、不用写 26 个 if。
⚠ 唯一要小心的是那个 +1(第 ③ 步那个错法:少了它,含 A 的名字整个变成 0)。
★ 而这道题最值得学的一步,其实是动笔之前那一句乘法:
名字最长 6 个字母 ⇒ 积最大 26⁶ = 308 915 776,只有 int 上限的 14.4%
⇒ 「边乘边取模」和「先乘完再取模」在这道题上都对。
// P1200 你的飞碟在这儿 —— 正解:字母变数字,连乘取模,比一比//// ★ 这道题是[第 47 章第 3 步](/ch/47-string-basics/)那句「字符本来就是整数」最直白的一次:// `c - 'A' + 1` 就是 A=1 … Z=26,**别去查表、别写 26 个 if**。// ⚠ 唯一要小心的是那个 **+1**:题面写的是「A 是 1,Z 是 26」,// 而 `c - 'A'` 给的是 0~25(见 p1200Zero.cpp —— 少了那个 1,含 A 的名字整个变成 0)。// ★ 一笔一眼的账:名字最长 6 个字母 ⇒ 积最大 26⁶ = 308 915 776 < 2³¹(余量 6.95 倍)// ⇒ **先乘完再取模也不会溢出**(见 p1200Big.cpp),这道题两种写法都对。#include <bits/stdc++.h>using namespace std;
static int val(const string& s) { int p = 1; for (char c : s) p = p * (c - 'A' + 1) % 47; // ★ 边乘边取模:怎么长都不会溢出 return p;}
int main() { string a, b; if (!(cin >> a >> b)) return 0; printf("%s\n", val(a) == val(b) ? "GO" : "STAY"); return 0;}点「运行 ▶」看结果
2★★★ 3.2 亿个名字全枚举 —— 顺带把那条溢出线量出来
// P1200 解析页上所有数字的出处。// ./p1200All 人话版// ./p1200All csv 给 check:viz 用//// ★★ 这道题的输入空间是「1~6 个大写字母」两个 ⇒ 单个名字只有// Σ 26^L = **321 272 406** 个(3.2 亿)—— 一个名字一个名字全跑一遍是几秒钟的事。// ⇒ [「输入空间小的时候,算一遍比对拍又快又充分」](/sol/p1002/)的又一次。//// 它算三件事:// ① **「边乘边取模」和「先乘完再取模」在题面范围内是不是永远一样** ——// 3.2 亿个名字逐个比(答案:一个都不差,因为 26⁶ 只有 `int` 上限的 14.4%);// ② ⚠ 那条线在哪:把名字放长到 7 个字母,**最小的会让 int 版出错的积**是多少;// ③ ★★ **随机两个名字答 GO 的概率** —— 这个数决定了顺手写的对拍在验什么:// 它只有 2% 上下,也就是说 300 轮里正解有 290 多轮在说 STAY。#include <bits/stdc++.h>using namespace std;
static long long cnt[47]; // 长度 1~6 的全部名字,积 mod 47 的分布static long long names = 0, nomatch = 0, maxProd = 0;
/** DFS:p 是「边乘边取模」的值,raw 是「不取模」的真实积 */static void dfs(int depth, int pmod, long long raw) { if (depth > 0) { names++; cnt[pmod]++; if (raw > maxProd) maxProd = raw; if ((int)(raw % 47) != pmod) nomatch++; // 真实积 mod 47 ↔ 边乘边取模 } if (depth == 6) return; for (int c = 1; c <= 26; c++) dfs(depth + 1, pmod * c % 47, raw * c);}
int main(int argc, char** argv) { bool csv = argc > 1 && string(argv[1]) == "csv";
dfs(0, 1, 1);
/* ② 长度 7:枚举「七个因子的多重集」(C(32,7) = 3 365 856 个), 找出最小的、会让 int 版算错的那个积。⚠ 线不在「长度 7」上,在「积越过 2³¹」上。 */ long long INTMAX = 2147483647LL; long long minBad = LLONG_MAX; int badFactors[7] = {0}; int f[7]; function<void(int, int, long long)> pick = [&](int idx, int lo, long long prod) { if (idx == 7) { if (prod > INTMAX) { int asInt = (int)(unsigned)(unsigned long long)prod; // 32 位截断 if (asInt % 47 != (int)(prod % 47) && prod < minBad) { minBad = prod; for (int i = 0; i < 7; i++) badFactors[i] = f[i]; } } return; } for (int c = lo; c <= 26; c++) { if (prod * c > 20000000000LL) break; // 剪枝:比 26⁷ 还大就没必要往下走 f[idx] = c; pick(idx + 1, c, prod * c); } }; pick(0, 1, 1);
/* ③ 两个随机名字同余的概率 = Σ (c_r / N)² */ double collide = 0; for (int r = 0; r < 47; r++) { double p = (double)cnt[r] / (double)names; collide += p * p; }
/* ④ 官方样例 */ auto val = [](const string& s) { int p = 1; for (char c : s) p = p * (c - 'A' + 1) % 47; return p; };
long long p7 = 1; for (int i = 0; i < 7; i++) p7 *= 26;
if (csv) { printf("names,%lld\nmismatch,%lld\nmaxProd,%lld\n", names, nomatch, maxProd); printf("headroom,%.2f\n", (double)INTMAX / (double)maxProd); printf("pow7,%lld\nover7,%.2f\n", p7, (double)p7 / (double)INTMAX); printf("minBad,%lld\n", minBad); printf("minBadName,"); for (int i = 0; i < 7; i++) putchar('A' + badFactors[i] - 1); putchar('\n'); printf("collide,%.4f\n", collide * 100.0); printf("sample1,%d,%d\n", val("COMETQ"), val("HVNGAT")); printf("sample2,%d,%d\n", val("ABSTAR"), val("USACO")); return 0; }
printf("① 长度 1~6 的名字一共 %lld 个;「边乘边取模」和「真实积 mod 47」对不上 %lld 个\n", names, nomatch); printf(" 最大的积 = 26⁶ = %lld,只有 int 上限的 %.1f%%(余量 %.2f 倍)⇒ 先乘完再取模也安全\n", maxProd, 100.0 / ((double)INTMAX / (double)maxProd), (double)INTMAX / (double)maxProd); printf("② 放长到 7 个字母:26⁷ = %lld,是 int 上限的 %.2f 倍\n", p7, (double)p7 / (double)INTMAX); printf(" 最小的、会让 int 版算错的积是 %lld(一个这样的名字:", minBad); for (int i = 0; i < 7; i++) putchar('A' + badFactors[i] - 1); printf(")\n"); printf("③ 两个随机名字答 GO 的概率 = %.4f%%(1/47 = %.4f%%)\n", collide * 100.0, 100.0 / 47.0); printf("④ 官方样例:COMETQ %d ↔ HVNGAT %d(GO);ABSTAR %d ↔ USACO %d(STAY)\n", val("COMETQ"), val("HVNGAT"), val("ABSTAR"), val("USACO")); return 0;}点「运行 ▶」看结果
单个名字的输入空间是 Σ 26^L (L = 1..6) = 321 272 406 个(3.2 亿)
⇒ 全跑一遍只要 0.9 秒,比任何对拍都快、都全
(第 46 章 P1100 那页枚举过 42.9 亿,这次是 3.2 亿)。
| 问的问题 | 答案 |
|---|---|
| 「边乘边取模」和「真实积 mod 47」在题面范围内一样吗 | ★ 3.2 亿个名字,0 个不同 |
| 最大的积 | 26⁶ = 308 915 776(int 上限的 14.4%,余量 6.95 倍) |
| 放长到 7 个字母呢 | 26⁷ = 8 031 810 176,是 int 上限的 3.74 倍 |
| ★ 那条线具体在哪 | 最小的、会让 int 版算错的积是 2 147 516 800(例:PSSVYZZ) |
两个随机名字答 GO 的概率 |
★ 2.1739%(1/47 = 2.1277%) |
⇒ ★★ 第三、四行合起来才是完整的话:那条线不在「长度 7」上,在「积越过 2³¹」上 ——
七个字母的名字里也有一大堆是安全的(AAAAAAA 的积是 1)。
⇒ 这就是本书量了很多次的那件事:「要不要防溢出」是一道拿题面乘一遍的算术题
(第 43 章 P1044、第 46 章 P1582)——
⚠ 而这道题的答案是「不用,但它靠的是题面那句『不超过 6 个字母』」。
3⚠ 三个错法
题面写的是「A 是 1,Z 是 26」,而 c - 'A' 给的是 0~25。
★ 说清楚它算了什么:只要名字里出现一个 A,整个积就被乘成 0。
⇒ 两个名字都含 A 时它必答 GO,只有一个含 A 时它几乎必答 STAY。
⚠ 而不含 A 的名字它也在算另一个数(每个字母都小 1),只是取模之后偶尔会撞上。
// P1200 ✗ 错法一:`c - 'A'` —— A 变成了 0//// ⚠ 题面写的是「A 是 1,Z 是 26」,而 `c - 'A'` 给的是 0~25。// ★ 说清楚它算了什么:**只要名字里出现一个 A,整个积就被乘成 0**。// ⇒ 于是「两个名字都含 A」时它必答 GO(0 == 0),// 「只有一个含 A」时它几乎必答 STAY。// ⚠ 它不是「每一组都错」型 —— 不含 A 的名字它也只是把每个字母都算小 1,// 积完全不同,可**取模之后照样可能撞上**。#include <bits/stdc++.h>using namespace std;
static int val(const string& s) { int p = 1; for (char c : s) p = p * (c - 'A') % 47; // ⚠ 少了 + 1 return p;}
int main() { string a, b; if (!(cin >> a >> b)) return 0; printf("%s\n", val(a) == val(b) ? "GO" : "STAY"); return 0;}点「运行 ▶」看结果
题面要的是「两个数 mod 47 相等」。比积本身严格得多 ——
它几乎只在两个名字是同一堆字母时才说 GO。
★ 触发条件:两个积模 47 同余,但它们本身不相等。
纯粹的读题错误 —— 题面不但写了「所有字母的积」,还给了一行验算
(USACO = 21 × 19 × 1 × 3 × 15 = 17955)。
★ 触发条件:「和同余」和「积同余」这两件事结论不同。
⚠ 它和上一个正好相反:它比正解松,所以常常多答 GO。
4★★★ 对拍:顺手写的那一档在验零 —— 300 轮只有 12 轮说 GO
// P1200 的生成器:./p1200Gen 种子 [档位]//// ★ 三个错法各靠什么现形:// · p1200Zero(`c - 'A'`,A 变成 0)→ 两套算法结论不同(名字里有没有 A 是关键);// · p1200NoMod(忘了取模) → **两个积模 47 同余,但它们本身不相等**;// · p1200Sum(把积读成和) → 「和同余」和「积同余」这两件事结论不同。//// ⚠⚠ 而顺手写的那一档有个大问题:**随机两个名字答 GO 的概率只有 2.17%**// ⇒ 300 轮里正解有 290 多轮在说 STAY,两版**一起说 STAY** 算「通过」// ——[「一致有两种:都算对了,和都没算」](/sol/p1746/)。⇒ 必须专门造一档「保证 GO」。//// 档位:// 0 ★ 顺手写的:两个随机名字,长度 1~6// 1 ⚠ 保证 GO:先随机第一个,再摇第二个直到两者同余(期望 47 次)// 2 ⚠ 两个名字都只有**一个字母**(★ 那时「和」就是「积」⇒ p1200Sum 在这一档必然是 0)// 3 ★ 最终档:一半保证 GO、一半随机//// ⚠ rng() 一律先落到具名变量再传参([第 24 章 P1776](/sol/p1776/) 那一跤)。#include <bits/stdc++.h>using namespace std;
static mt19937 rng;
static string pick(int lo, int hi) { unsigned r = rng() % (unsigned)(hi - lo + 1); int len = lo + (int)r; string s; for (int i = 0; i < len; i++) { unsigned c = rng() % 26u; s.push_back((char)('A' + c)); } return s;}static int val(const string& s) { int p = 1; for (char c : s) p = p * (c - 'A' + 1) % 47; return p;}
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1; int mode = argc > 2 ? atoi(argv[2]) : 0; rng.seed(seed * 1000003u + 20260907u);
int lo = 1, hi = 6; if (mode == 2) { lo = 1; hi = 1; }
bool wantGo = (mode == 1); if (mode == 3) { unsigned f = rng() % 2u; wantGo = (f == 0); }
string a = pick(lo, hi), b = pick(lo, hi); if (wantGo) { for (int guard = 0; guard < 100000 && val(a) != val(b); guard++) b = pick(lo, hi); } printf("%s\n%s\n", a.c_str(), b.c_str()); return 0;}点「运行 ▶」看结果
四档 × 300 轮(正解 vs 先乘完再取模:1200 轮 0 组不一致):
| 档位 | 正解答 GO 的轮数 |
✗ A = 0 |
✗ 没取模 | ✗ 求和 |
|---|---|---|---|---|
| 0 ★ 顺手写的(两个随机名字) | ⚠ 12 | 23 | 12 | 19 |
1 ⚠ 反着造,保证 GO |
300 | 284 | 283 | 285 |
| 2 ⚠ 两个名字都只有一个字母 | 14 | ★ 0 | ★ 0 | ★ 0 |
3 ★ 最终档(一半保证 GO) |
160 | 155 | 147 | 151 |
-
★★★ 顺手写的那一档在验零 —— 300 轮里正解有 288 轮说
STAY, 而三个错法也倾向于说STAY⇒ 两边一起答对一半,对拍记「通过」。 ⇒ 「一致有两种:都算对了,和都没算」的又一次, ★ 而这一次那个比例是事先算得出来的:随机两个名字同余的概率 2.17%。 ⇒ ★★ 动手写生成器之前先算一句「正解会答 YES 的比例是多少」 —— 低于百分之几,就必须反着造(第 13 章 P1162 那条)。 一档之隔,三个错法从 23 / 12 / 19 跳到 284 / 283 / 285。 -
★★★ 档 2(两个名字都只有一个字母)把三个错法一起打成 0,而三个 0 都能证:
错法 为什么在单字母上必然对 A = 0两个单字母比大小, x+1 == y+1和x == y是同一件事没取模 单字母的积 ≤ 26,比 47 小 ⇒ 取模什么都没改 求和 ★ 单个字母的「和」就是它的「积」 ⇒ ★★ 第 33 章 P1266 那条的又一次:一个对照档同时给三个 0 交代清楚, 而且不用跑程序就能说明白。 ⚠ 反过来说:只拿最短的数据测,这三个 bug 一个都测不出来。
-
★ 十二格「触发 ≡ 抓获」一个不差 —— 三条触发条件各是一句精确的话(见上一步)。
5★ 官方样例:样例一把三个错法全打死
✗ A = 0 |
✗ 没取模 | ✗ 求和 | |
|---|---|---|---|
样例一 COMETQ / HVNGAT → GO |
★ 死(STAY) |
★ 死(STAY) |
★ 死(STAY) |
样例二 ABSTAR / USACO → STAY |
★ 死(GO) |
放过 | 放过 |
★ 原因干净得可以直接写出来:三个错法都倾向于说 STAY(A=0 是碰巧、
另外两个是结构性的),而样例一恰好是唯一那组答 GO 的 ⇒ 一测就死。
⇒ ★★ 这和上一步那张表说的是同一件事,只是换了个说法: 这道题的鉴别力全在「答 GO 的那些输入」上,而随机数据里它们只占 2%。 ⚠ 于是出题人给的两组样例比顺手写的 300 轮对拍还狠 —— 第 41 章 P1217 那条「出题人是照着边界挑样例的」的又一次。
6★ 哪一版就已经能过了
| 版本 | 结果 | 说明 |
|---|---|---|
★ p1200.cpp(边乘边取模) |
AC | 怎么长都不会溢出 |
★ p1200Big.cpp(先乘完) |
AC | 靠题面那句「≤ 6 个字母」(余量 6.95 倍) |
✗ A = 0 |
WA | 两组样例都死 |
| ✗ 没取模 | WA | 样例一就死 |
| ✗ 求和 | WA | 样例一就死 |
⇒ ★★ 一句话带走:这道题写起来五分钟,而它值得留下的是两个「先算一句」的习惯 ——
动笔前算一句「积最大多少」(决定要不要边乘边取模),
写生成器前算一句「正解答 GO 的比例是多少」(决定要不要反着造)。
两句都是一行乘法,而少了哪一句,都会得出一个看起来很正常的错误结论。