0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1618,日期见页头。两边不一致时信原站。
题目描述
将 1, 2, …, 9 共 9 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是
A : B : C,试求出所有满足条件的三个三位数,若无解,输出 No!!!。
输入格式:三个数,A、B、C。
输出格式:若干行,每行 3 个数字。按照每行第一个数字升序排列。
数据范围:保证 0 ≤ A < B < C ≤ 999。
upd 2022.8.3:新增加二组 Hack 数据。
输入输出样例
输入
1 2 3
输出
192 384 576 219 438 657 273 546 819 327 654 981
1 : 2 : 3 有四组解。上面那段输出是仓库里的 p1618.cpp 真跑出来的。
1第 ① 版:题目怎么说就怎么写(对,但跑不完)
「分成三组、组成三个三位数」—— 那就三个三位数各枚举一遍,每组验两件事:
九个数字恰好用一次、比例正好是 A : B : C。
// P1618 的第 ① 版:题目怎么说,代码就怎么写 —— 三个三位数全枚举一遍//// 「将 1~9 共 9 个数分成三组,组成三个三位数」⇒ 三个三位数各枚举一遍,// 每一组都验两件事:九个数字恰好用一次、比例正好是 A:B:C。//// ⚠ 比例**不要写成除法**(`x / A == y / B` 整除会截断,一堆假答案)。// 写成交叉相乘:x : y = A : B ⇔ x * B == y * A。// ★ 顺带把 A = 0 也接住了:B > A = 0,于是 x * B == y * 0 = 0 永不成立,// 自然输出 No!!! —— 而下一版栽的就是这里。//// 它是对的。它也是**跑不完的**:900³ ≈ 7.29 亿组,正文第 ③ 步有秒表。
#include <bits/stdc++.h>using namespace std;
// 九个数字恰好用一次(出现 0 就作废)bool okDigits(int x, int y, int z) { int used = 0; for (int v : {x, y, z}) for (int i = 0; i < 3; i++, v /= 10) { int d = v % 10; if (d == 0) return false; if (used >> d & 1) return false; used |= 1 << d; } return true;}
int main() { int A, B, C; cin >> A >> B >> C; bool any = false; for (int x = 100; x <= 999; x++) for (int y = 100; y <= 999; y++) for (int z = 100; z <= 999; z++) if (okDigits(x, y, z) && x * B == y * A && x * C == z * A) { printf("%d %d %d\n", x, y, z); any = true; } if (!any) printf("No!!!\n"); return 0;}点「运行 ▶」看结果
本机实测(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-26,独占):
900³ ≈ 7.29 亿组,8.52 秒。题目限时 1 秒。
x / A == y / B 是错的:整数除法会截断,199 / 2 == 200 / 2 都成立。
判比例一律交叉相乘:x : y = A : B ⇔ x * B == y * A。
★ 顺带一个便宜:A 可以是 0(题目只保证 0 ≤ A),
而交叉相乘把它当乘数,x * B == y * 0 永不成立,自然就输出 No!!! 了。
下一版栽的正是这一处 —— 记住这个便宜是从哪儿来的。
2第 ② 版:能算出来的量不要枚举(快了,样例也过了)
第 5 章的枚举第一原则:能算出来的量,不要枚举。
定了第一个数 x,比例就把后两个钉死了 —— 于是三重循环塌成一重:
for (int x = 100; x <= 999; x++) {
if (x % A != 0) continue; // 倍数 k = x / A 必须是整数
int k = x / A, y = k * B, z = k * C;
...
}
// P1618 的第 ② 版:只枚举第一个数,另外两个**算**出来//// 第 5 章的第一原则:**能算出来的量,不要枚举。**// 定了 x,比例就把 y、z 钉死了:y = x / A * B、z = x / A * C。// 900 万分之一的工作量,样例一秒不到就过。//// ⚠⚠ 可它是**错的**,而且样例挡不住 —— 两处:// ① `x % A != 0` 就跳过 ⇒ 默认「倍数 k = x / A 一定是整数」。// 可题目给的是**比例**:A:B:C = 2:4:6 和 1:2:3 是同一个比例,// 而 123 : 246 : 369 正好是 1:2:3 —— 它对 A = 2 不整除,于是被漏掉。// (洛谷 2022-08-03 加的两组 Hack 数据打的就是这里。)// ② A 可以是 0(题目只保证 0 ≤ A < B < C)⇒ `x % A` 当场除以 0,// x86 上直接 SIGFPE,一分不剩。
#include <bits/stdc++.h>using namespace std;
bool okDigits(int x, int y, int z) { int used = 0; for (int v : {x, y, z}) for (int i = 0; i < 3; i++, v /= 10) { int d = v % 10; if (d == 0) return false; if (used >> d & 1) return false; used |= 1 << d; } return true;}
int main() { int A, B, C; cin >> A >> B >> C; bool any = false; for (int x = 100; x <= 999; x++) { if (x % A != 0) continue; // ⚠ A = 0 时这一行就是除以 0 int k = x / A; int y = k * B, z = k * C; if (y > 999 || z > 999) break; if (okDigits(x, y, z)) { printf("%d %d %d\n", x, y, z); any = true; } } if (!any) printf("No!!!\n"); return 0;}点「运行 ▶」看结果
A : B : C = 2 : 4 : 6 和 1 : 2 : 3 是同一个比例。
可这一版要求 x % A == 0:
输入 1 2 3 -> 192 384 576 / 219 438 657 / 273 546 819 / 327 654 981 ✓ 四行
输入 2 4 6 -> 192 384 576 ✗ 只剩一行219 : 438 : 657 确实等于 2 : 4 : 6,但 219 不是 2 的倍数 —— 它被 continue 掉了。
洛谷 2022 年 8 月加的那两组 Hack 数据,打的就是这里。
3★★★ 把 (A, B, C) 的整个空间扫一遍:它到底错多少
「漏一点」还是「错一片」,是两件事。这道题的输入空间小得可以整个扫完,那就别猜:
// 把 (A, B, C) 的**整个空间**扫一遍:整除版到底漏了多少组//// 用法:./p1618Sweep 人话版// ./p1618Sweep csv 只打 `键,值`,给 check:viz 用//// ★ 为什么能扫得动:A < B < C ≤ 999 有 C(1000,3) = 166 167 000 组,// 一组组去解是不可能的。反过来做就很便宜:// ① 1~9 的全排列只有 9! = 362 880 种,切成三个三位数就是**全部可能的解**;// ② 一组解 (x, y, z) 除掉它们的 gcd,得到**这组解唯一的最简比例** (a, b, c);// ③ 于是「有解的 (A,B,C)」= 某个最简比例的整数倍,t 从 1 数到 999 / c 就完了。// ⇒ 有解的组数其实少得可怜,正文那张表里的数就是这么来的。//// 整除版(p1618Div.cpp)的判据是 `x % A == 0`,即「倍数 k = x / A 必须是整数」。// 而 (A,B,C) = t·(a,b,c)、解 (x,y,z) = s·(a,b,c) 时,这个判据等价于 **t | s** ——// t 不整除 s 的那些解,它一个都看不见。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { bool csv = (argc > 1 && string(argv[1]) == "csv");
// ① 全排列 -> 所有可能的解,按最简比例分组 map<array<int, 3>, vector<array<int, 3>>> byRatio; int digits[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; long long perms = 0, sols = 0; do { perms++; int x = digits[0] * 100 + digits[1] * 10 + digits[2]; int y = digits[3] * 100 + digits[4] * 10 + digits[5]; int z = digits[6] * 100 + digits[7] * 10 + digits[8]; if (!(x < y && y < z)) continue; // A < B < C ⇒ 必然 x < y < z sols++; int g = __gcd(__gcd(x, y), z); byRatio[{x / g, y / g, z / g}].push_back({x, y, z}); } while (next_permutation(digits, digits + 9));
// ② 每个最简比例的整数倍,就是所有「有解」的 (A, B, C) long long solvable = 0, missedTriples = 0, missedLines = 0, missGcd = 0; array<int, 3> firstMissed = {0, 0, 0}; for (auto& [r, list] : byRatio) { for (int t = 1; (long long)t * r[2] <= 999; t++) { solvable++; int A = t * r[0]; long long miss = 0; for (auto& s : list) if (s[0] % A != 0) miss++; /* 约分版(p1618Gcd.cpp):t 被除掉了,判据变成「x 是 a 的倍数」,恒成立 */ for (auto& s : list) if (s[0] % r[0] != 0) missGcd++; if (miss) { missedTriples++; missedLines += miss; if (!firstMissed[2] || array<int, 3>{A, t * r[1], t * r[2]} < firstMissed) firstMissed = {A, t * r[1], t * r[2]}; } } }
long long total = 1000LL * 999 * 998 / 6; // C(1000,3):A < B < C ≤ 999 long long zeroA = 999LL * 998 / 2; // A = 0 的那些:整除版当场除以 0 if (csv) { printf("perms,%lld\nsolutions,%lld\nratios,%zu\ntotal,%lld\nsolvable,%lld\n" "missedTriples,%lld\nmissedLines,%lld\nmissedGcd,%lld\nfirstMissed,%d %d %d\nzeroA,%lld\n", perms, sols, byRatio.size(), total, solvable, missedTriples, missedLines, missGcd, firstMissed[0], firstMissed[1], firstMissed[2], zeroA); } else { printf("1~9 的全排列 %lld 种\n", perms); printf("其中 x < y < z 的(= 全部可能的解)%lld 组\n", sols); printf("它们一共只有 %zu 个最简比例\n", byRatio.size()); printf("A < B < C <= 999 一共 %lld 组\n", total); printf(" 其中有解的只有 %lld 组\n", solvable); printf(" 整除版会漏掉解的 %lld 组(共漏 %lld 行答案)\n", missedTriples, missedLines); printf(" 而约分版(p1618Gcd)漏的是 %lld 行\n", missGcd); printf(" 最小的那一组是 %d %d %d\n", firstMissed[0], firstMissed[1], firstMissed[2]); printf("另外 A = 0 的 %lld 组,整除版直接除以 0\n", zeroA); } return 0;}点「运行 ▶」看结果
| 组数 | |
|---|---|
A < B < C ≤ 999 一共 |
166 167 000 组 |
| 其中有解的 | 98 199 组(万分之六) |
| 其中第 ② 版会漏解的 | ★ 26 081 组(有解的组里 26.6%) |
| 最小的那一组 | ★ 2 4 6 |
另外 A = 0 的 |
498 501 组,第 ② 版当场除以 0 |
x % A 在 A = 0 时是除以零。x86 上直接 SIGFPE(退出码 136),
一个字都没输出。⇒ 页面上把输入改成 0 1 2 试一次,第 ② 版会当场炸给你看。
★ 这一类特判有个共性:题目把它写在数据范围里,而不是写在描述里
(「保证 0 ≤ A」)—— 数据范围是题面的一部分,要一个字一个字读。
4第 ③ 版:一行修好 —— 先约分(能 AC,但只修好了一半)
第 ② 版漏解的唯一原因是它默认「x 一定是 A 的整数倍」。
写成式子就清楚了:(A,B,C) = t·(a,b,c)、解 (x,y,z) = s·(a,b,c),
它的判据等价于 t | s —— 而 t = gcd(A,B,C) 就是那个多余的 t。除掉它:
int g = __gcd(__gcd(A, B), C);
A /= g; B /= g; C /= g; // ★ 就这一行
★ 它已经能 AC 了(check:viz 扫过全空间:漏解 0 行)。
⚠ 可 A = 0 那 49 万组还在:gcd(0, B, C) = gcd(B, C),A 除完还是 0。
修好了一个坑,另一个坑一动没动。
5★ 第 ④ 版:换一种写法,两个坑一起消失
回到第 ① 版那个便宜:判比例用交叉相乘,A 是乘数不是除数。
把它和「只枚举 x」合起来 —— y = x·B / A,除得尽才算数:
long long y = 1LL * x * B, z = 1LL * x * C;
if (y % A || z % A) continue; // 除不尽 ⇒ 这个 x 配不出整数
y /= A; z /= A;
// P1618 三连击(升级版)—— 能 AC 的那一版//// 只枚举第一个数 x(900 个),另外两个用**交叉相乘**反解,一次除法都不做:// x : y : z = A : B : C// ⇔ y * A == x * B 且 z * A == x * C//// 所以 y 必须满足 A | x*B,取 y = x * B / A,再回代验一次(验的是「除得尽」)。// ★ 这么写,三件事同时被解决:// ① 比例不用约分(2:4:6 和 1:2:3 自动等价);// ② 倍数 k 不必是整数(123 : 246 : 369 照样能被找到);// ③ A = 0 也不崩 —— A 是**乘数**不是除数,x*B == y*0 永不成立,自然输出 No!!!。//// 枚举 x 是递增的 ⇒ 输出天然「按每行第一个数字升序」,不用再排序。
#include <bits/stdc++.h>using namespace std;
bool okDigits(int x, int y, int z) { int used = 0; for (int v : {x, y, z}) for (int i = 0; i < 3; i++, v /= 10) { int d = v % 10; if (d == 0) return false; // 含 0 的不行,只能用 1~9 if (used >> d & 1) return false; // 重复的不行 used |= 1 << d; } return true;}
int main() { int A, B, C; cin >> A >> B >> C; bool any = false; // A = 0 ⇒ 第一个数只能是 0,不可能是三位数。★ 这一行也是「别把 A 当除数」的收益: // 它只是一句提前收工,而不是一处「忘了就 RE」的特判。 for (int x = 123; A > 0 && x <= 987; x++) { long long y = 1LL * x * B, z = 1LL * x * C; if (y % A || z % A) continue; // 除不尽 ⇒ 这个 x 配不出整数 y /= A; z /= A; if (y > 999 || z > 999) break; // z 只会越来越大,可以直接收工 if (okDigits(x, (int)y, (int)z)) { printf("%d %lld %lld\n", x, y, z); any = true; } } if (!any) printf("No!!!\n"); return 0;}点「运行 ▶」看结果
这一版和第 ③ 版一样快、一样能过,但它不需要你记住任何一条特判:
| 第 ② 版 | 第 ③ 版(约分) | 第 ④ 版(交叉相乘) | |
|---|---|---|---|
2 : 4 : 6 这种没约分的比例 |
✗ 漏解 | ✓ 记得约分才行 | ✓ 天然对 |
A = 0 |
✗ 除以 0 | ✗ 除以 0 | ✓ 天然对 |
⇒ 第 ③ 版是「我记得要约分」,第 ④ 版是「不需要记」。 第 47 章那一课在这儿又出现了一次: 一个坑要靠你记住才不踩,它就迟早会被踩;能改成「压根不存在」的,就别留着。
6★★★ 对拍:顺手写的生成器一轮都抓不到
三个版本摆在这儿,对拍应该能把第 ② 版打假吧?顺手写一个生成器试试——
随机 1 ≤ A < B < C ≤ 999,300 轮:
// 数据生成器(P1618 对拍用):`./p1618Gen <seed> [level]`//// level 0(默认)顺手写法:随机 1 <= A < B < C <= 999// level 1 ★ 反着造:先随机一个 1~9 的排列当**答案**,// 切成三个三位数、排好序、除掉 gcd 得到最简比例 (a,b,c),// 再随机乘一个 t,输出 (ta, tb, tc) —— 这样造出来的 (A,B,C) **保证有解**。// level 2 ★★ 反着造,而且**逼着 t >= 2**:只留下 c <= 499 的比例(否则 t 只能是 1)。//// ★★★ 为什么非要有 level 1(这道题最值钱的一条,正文第 ⑥ 步):// A < B < C <= 999 一共 166 167 000 组,**有解的只有 98 199 组** —— 万分之六。// 顺手随机 300 轮,几乎每一轮三个版本都齐刷刷输出 No!!!,**对拍全绿而错版还是错的**。// 要抓漏解,必须**先把「有答案」造出来**。
#include <bits/stdc++.h>using namespace std;
static mt19937 rng;static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) { unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1; int level = (argc > 2) ? atoi(argv[2]) : 0; rng.seed(seed);
if (level == 0) { int a = ri(1, 997), b = ri(a + 1, 998), c = ri(b + 1, 999); printf("%d %d %d\n", a, b, c); return 0; }
int d[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; shuffle(d, d + 9, rng); int v[3] = {d[0] * 100 + d[1] * 10 + d[2], d[3] * 100 + d[4] * 10 + d[5], d[6] * 100 + d[7] * 10 + d[8]}; sort(v, v + 3); int g = __gcd(__gcd(v[0], v[1]), v[2]); int a = v[0] / g, b = v[1] / g, c = v[2] / g; if (level >= 2) { // ★★ level 1 还不够:整除版只在 t >= 2 时才漏,而最简比例的 c 多半有七八百, // 999 / c 就是 1 —— t 被**逼成** 1,于是「保证有解」的数据照样打不假。 // 这里重摇到 c <= 499 为止,t 才有得选。 while (c > 499) { shuffle(d, d + 9, rng); v[0] = d[0] * 100 + d[1] * 10 + d[2]; v[1] = d[3] * 100 + d[4] * 10 + d[5]; v[2] = d[6] * 100 + d[7] * 10 + d[8]; sort(v, v + 3); g = __gcd(__gcd(v[0], v[1]), v[2]); a = v[0] / g; b = v[1] / g; c = v[2] / g; } } int t = (level >= 2) ? ri(2, 999 / c) : ri(1, 999 / c); // ⚠ t = 1 时整除版是对的 printf("%d %d %d\n", a * t, b * t, c * t); return 0;}点「运行 ▶」看结果
本机实测(check:viz 每次都真跑一遍):
| 生成器 | 造出来的是什么 | 300 轮里第 ② 版被抓 |
|---|---|---|
档位 0:随机 A < B < C |
万分之六才有解,其余全是 No!!! |
★ 0 轮 |
档位 1:反着造(先造答案,再乘 t) |
保证有解 | 15 轮 |
档位 2:反着造,并且逼着 t ≥ 2 |
保证有解、且没约分 | ★ 171 轮 |
- 档位 0 全绿。 因为 1.66 亿组里只有 9.8 万组有解 —— 随机 300 轮,
三个版本齐刷刷输出
No!!!,错的那版和对的那版一模一样地对。 ⇒ 第 49 章那条:调生成器的第一步,是把「有答案」造出来。 - ★ 档位 1 只抓到 15 轮,而它已经「保证有解」了。
为什么?第 ② 版只在
t ≥ 2(比例没约分)时才漏,而最简比例的c多半有七八百,t最大只能取999 / c = 1——t被逼成了 1。 「保证有解」的数据,照样打不假。 - 档位 2 抓 171 轮。 只多做一件事:重摇到
c ≤ 499,让t有得选。
⇒ 一句话:造对「有答案」只是第一步,还要造得到「能踩到那个 bug 的答案」。 (第 51 章是「要又大又深」,第 52 章是「顺手的生成器 会亲手删掉一个边界」,这一页是它自己把触发条件挤没了。)
7四个版本并排
| 版本 | 做法 | 枚举量 | 1 2 3 |
2 4 6 |
0 1 2 |
能过吗 |
|---|---|---|---|---|---|---|
① p1618Brute |
三个三位数全枚举 | 7.29 亿 | ✓ | ✓ | ✓ | ✗ 8.52 秒 |
② p1618Div |
只枚举 x,x % A == 0 |
900 | ✓ | ✗ 漏 3 行 | ✗ RE | ✗ |
③ p1618Gcd |
② + 先约分 | 900 | ✓ | ✓ | ✗ RE | ✗ |
④ p1618 |
只枚举 x,交叉相乘 |
900 | ✓ | ✓ | ✓ | ★ 能 |
- 无解要输出
No!!!—— 三个感叹号,一个不多一个不少。 - 「按每行第一个数字升序」:枚举
x是从小到大的,天然就是升序,不用再排序。 ⚠ 但如果你是枚举倍数k而不是x,顺序就得自己想清楚了。
- ★★ 判比例一律交叉相乘。 除法会截断(漏解),也会除以 0(RE)—— 而这两件事样例都挡不住,第 ② 版在样例上是满分表现。
- ★ 「能算出来的量不要枚举」是对的,但「算」要算对。 同样是一重循环,第 ② 版和第 ④ 版差着 26 081 组数据。
- ★★★ 顺手写的生成器一轮都抓不到(0/300);改成「先造答案」也只抓到 15 轮 ——
因为它把触发条件
t ≥ 2自己挤没了。 逼出t ≥ 2之后是 171 轮。