题单 · 习题解析

洛谷 P1200 [USACO1.1] 你的飞碟在这儿

★★★ 这一页真正的收获是两个「先算一句」的习惯:动笔前算「积最大多少」(`26⁶` = 308 915 776 只有 `int` 上限的 **14.4%** ⇒ 边乘边取模和先乘完再取模**都对**,3.2 亿个名字全枚举 0 个不同);写生成器前算「正解答 GO 的比例是多少」(★ **2.1739%** ⇒ 顺手写的那一档 300 轮只有 **12 轮**说 GO,三个错法**也倾向于说 STAY** ⇒ 对拍在验零,抓获数只有 23 / 12 / 19,而反着造保证 GO 之后立刻变成 **284 / 283 / 285**);★★★ 「两个名字都只有一个字母」那一档把三个错法**一起打成精确的 0,而三个 0 都能不跑程序地证明**(`x+1 == y+1` ⟺ `x == y` / 积 ≤ 26 < 47 ⇒ 取模没改什么 / **单个字母的和就是它的积**)⇒ [第 33 章 P1266](/sol/p1266/) 那条「一个对照档同时交代三个 0」的又一次,反过来说就是**只拿最短的数据测,这三个 bug 一个都测不出来**;★★ 溢出那条线不在「长度 7」上而在「积越过 2³¹」上 —— `26⁷` = 8 031 810 176 是 `int` 上限的 3.74 倍,而**最小的、会让 int 版算错的积是 2 147 516 800**(例:`PSSVYZZ`);★ 十二格「触发 ≡ 抓获」一个不差;★★ 官方样例一(唯一那组 GO)把三个错法**全打死** —— 出题人挑的两组,比顺手写的 300 轮还狠

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

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

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 的大写字母串,表示队伍的名字。

输出格式

一行内输出 GOSTAY

数据范围

USACO Training Section 1.1。时限 1 秒,内存 128000 KB(125 MB)。

输入输出样例

输入

COMETQ
HVNGAT

输出

GO

两个名字算出来 mod 47 都是 27GO

输入

ABSTAR
USACO

输出

STAY

ABSTAR mod 47 = 3,USACO mod 47 = 1 ⇒ STAY

1★ 一句话:字符本来就是整数,这道题连表都不用查

★ 全部内容就是 c - 'A' + 1

第 47 章第 3 步那句「字符存的就是它的编码」在这里直接就是答案: A 是 1、Z 是 26 ⇒ 写 c - 'A' + 1不用查表、不用写 26 个 if

⚠ 唯一要小心的是那个 +1(第 ③ 步那个错法:少了它,含 A 的名字整个变成 0)。

★ 而这道题最值得学的一步,其实是动笔之前那一句乘法: 名字最长 6 个字母 ⇒ 积最大 26⁶ = 308 915 776,只有 int 上限的 14.4%「边乘边取模」和「先乘完再取模」在这道题上都对。

p1200.cpp★ 正解:边乘边取模
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1200Big.cpp★ 另一种正确写法:先乘完,最后取模

2★★★ 3.2 亿个名字全枚举 —— 顺带把那条溢出线量出来

p1200All.cpp★ 长度 1~6 的全部名字跑一遍(0.9 秒)
// 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 776int 上限的 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⚠ 三个错法

✗ 错法一:c - 'A' —— A 变成了 0

题面写的是「A 是 1,Z 是 26」,而 c - 'A' 给的是 0~25。 ★ 说清楚它算了什么:只要名字里出现一个 A,整个积就被乘成 0。 ⇒ 两个名字都含 A 时它必答 GO,只有一个含 A 时它几乎必答 STAY。 ⚠ 而不含 A 的名字它也在算另一个数(每个字母都小 1),只是取模之后偶尔会撞上

p1200Zero.cpp✗ 错法一:A = 0
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
✗ 错法二:忘了取模,直接比两个积

题面要的是「两个数 mod 47 相等」。比积本身严格得多 —— 它几乎只在两个名字是同一堆字母时才说 GO。 ★ 触发条件:两个积模 47 同余,但它们本身不相等。

p1200NoMod.cpp✗ 错法二:没取模
✗ 错法三:把「积」读成了「和」

纯粹的读题错误 —— 题面不但写了「所有字母的」,还给了一行验算 (USACO = 21 × 19 × 1 × 3 × 15 = 17955)。 ★ 触发条件:「和同余」和「积同余」这两件事结论不同。 ⚠ 它和上一个正好相反:它比正解松,所以常常多答 GO

p1200Sum.cpp✗ 错法三:求和不是求积

4★★★ 对拍:顺手写的那一档在验零 —— 300 轮只有 12 轮说 GO

p1200Gen.cpp★ 生成器:档 1 反着造,直接保证答案是 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
★★★ 三条读得出来的结论
  1. ★★★ 顺手写的那一档在验零 —— 300 轮里正解有 288 轮STAY, 而三个错法也倾向于说 STAY ⇒ 两边一起答对一半,对拍记「通过」。 ⇒ 「一致有两种:都算对了,和都没算」的又一次, ★ 而这一次那个比例是事先算得出来的:随机两个名字同余的概率 2.17%。 ⇒ ★★ 动手写生成器之前先算一句「正解会答 YES 的比例是多少」 —— 低于百分之几,就必须反着造(第 13 章 P1162 那条)。 一档之隔,三个错法从 23 / 12 / 19 跳到 284 / 283 / 285。

  2. ★★★ 档 2(两个名字都只有一个字母)把三个错法一起打成 0,而三个 0 都能证

    错法 为什么在单字母上必然对
    A = 0 两个单字母比大小,x+1 == y+1x == y 是同一件事
    没取模 单字母的积 ≤ 26,比 47 小 ⇒ 取模什么都没改
    求和 ★ 单个字母的「和」就是它的「积」

    ⇒ ★★ 第 33 章 P1266 那条的又一次:一个对照档同时给三个 0 交代清楚, 而且不用跑程序就能说明白。 ⚠ 反过来说:只拿最短的数据测,这三个 bug 一个都测不出来。

  3. 十二格「触发 ≡ 抓获」一个不差 —— 三条触发条件各是一句精确的话(见上一步)。

5★ 官方样例:样例一把三个错法全打死

★★ 而它靠的是「它是那组 GO」
A = 0 ✗ 没取模 ✗ 求和
样例一 COMETQ / HVNGATGO STAY STAY STAY
样例二 ABSTAR / USACOSTAY GO 放过 放过

★ 原因干净得可以直接写出来:三个错法都倾向于说 STAYA=0 是碰巧、 另外两个是结构性的),而样例一恰好是唯一那组答 GO 的 ⇒ 一测就死。

⇒ ★★ 这和上一步那张表说的是同一件事,只是换了个说法: 这道题的鉴别力全在「答 GO 的那些输入」上,而随机数据里它们只占 2%。 ⚠ 于是出题人给的两组样例比顺手写的 300 轮对拍还狠 —— 第 41 章 P1217 那条「出题人是照着边界挑样例的」的又一次。

6★ 哪一版就已经能过了

★ 两种写法都是终点
版本 结果 说明
p1200.cpp(边乘边取模) AC 怎么长都不会溢出
p1200Big.cpp(先乘完) AC 靠题面那句「≤ 6 个字母」(余量 6.95 倍)
A = 0 WA 两组样例都死
✗ 没取模 WA 样例一就死
✗ 求和 WA 样例一就死

⇒ ★★ 一句话带走:这道题写起来五分钟,而它值得留下的是两个「先算一句」的习惯 —— 动笔前算一句「积最大多少」(决定要不要边乘边取模), 写生成器前算一句「正解答 GO 的比例是多少」(决定要不要反着造)。 两句都是一行乘法,而少了哪一句,都会得出一个看起来很正常的错误结论。