题单 · 习题解析

洛谷 P1029 [NOIP 2001 普及组] 最大公约数和最小公倍数问题

★★ 关键一步两行:设 P = x₀a、Q = x₀b ⇒ gcd(a,b) = 1 且 **a·b = y₀/x₀** ⇒ 枚举因子 O(√n);★★★ 再推一步 —— 每个质因子的整块幂只能整个给一边 ⇒ **答案 = 2^ω(n)**(题面范围内上界只有 **64**);★★ 而这个公式不用「相信」:x₀, y₀ ≤ 120 的**全部 14161 组**逐组拿 O(y₀²) 暴力核对,**0 组不一致**、482 组有解**全是 2 的幂**;⚠⚠ 最值钱的是那张对拍表 —— 「先挑 x₀ 再挑倍数当 y₀」这个**所有人都会顺手写的「贴心」生成器,把「忘了判 y₀ % x₀」那个错法打成精确的 0**(照题面随机是 292/300)⇒ **生成器照顾了输入的合法性,就等于把「程序自己该判合法性」测没了**;★ 而照题面随机那一档 300 轮只有 **8 组有解** ⇒ 另外 292 轮两边一起输出 0,验的是零 ⇒ **两档都要有**;★ 四个格子「触发 ≡ 抓获」一个不差(292≡292 / 3≡3 / 14≡14 / 300≡300);⚠ 暴力外推顶格约 318 秒、正解 0.21 微秒

原题:洛谷 P1029出自 第 40 章 GCD、LCM 与欧几里得算法 的题单题面本地存档:2026-09-01
⚠ 先自己写一遍,再往下看

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

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1029,日期见页头。两边不一致时信原站。

题目描述

输入两个正整数 x₀y₀,求出满足下列条件的 PQ 的个数:

  1. PQ 是正整数。
  2. 要求 PQx₀最大公约数,以 y₀最小公倍数

试求:满足条件的所有可能的 PQ 的个数。

输入格式

一行两个正整数 x₀y₀

输出格式

一行一个数,表示求出满足条件的 PQ 的个数。

数据范围

对于 100% 的数据,2 ≤ x₀, y₀ ≤ 10⁵

★ 说明:PQ 有 4 种 —— ① 3, 60;② 15, 12;③ 12, 15;④ 60, 3。 ⇒ 它数的是有序对(15,12)(12,15) 算两个)。

时限 1 秒,内存 125 MB。(题目来源:NOIP 2001 普及组第二题)

输入输出样例

输入

3 60

输出

4

x₀ = 3(最大公约数)、y₀ = 60(最小公倍数)⇒ 上面那四组。

1第 ① 版:老老实实枚举 P 和 Q

p1029Brute.cpp第 ① 版:双重枚举 O(y₀²) —— 参照物,但一分都拿不到
// P1029 第 ① 版:老老实实枚举 P 和 Q
//
// ★ 它是参照物。P、Q 都不会超过 y₀(lcm 是两者的倍数)⇒ 双重循环 O(y₀²)。
// 题面 y₀ ≤ 10⁵ ⇒ 最坏 10¹⁰ 次,**过不去**;但小数据上它是最可信的那一份。
//
// ⚠ 这道题**没有分档**(题面只有一句「100% 的数据 2 ≤ x₀, y₀ ≤ 10⁵」)
// ⇒ 和[第 39 章 P1531](/sol/p1531/) 一样:暴力一分都拿不到。
#include <bits/stdc++.h>
using namespace std;
static long long gcdll(long long a, long long b) { while (b) { long long t = a % b; a = b; b = t; } return a; }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long x0, y0;
if (!(cin >> x0 >> y0)) return 0;
long long cnt = 0;
for (long long p = 1; p <= y0; p++)
for (long long q = 1; q <= y0; q++) {
long long g = gcdll(p, q);
if (g == x0 && p / g * q == y0) cnt++; // ⚠ 先除后乘:p*q 最大 10¹⁰
}
cout << cnt << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

PQ 都不会超过 y₀lcm 是两者的倍数)⇒ 双重循环就能数完。

⚠ 但这道题没有分档(题面只有一句「100% 的数据」)—— 本机实测:y₀ = 4000 时它就要 508 毫秒,按 y₀² 外推到顶格约 318 秒 ⇒ 和第 39 章 P1531 一样,0 分。 ★ 它的身份只有一个:参照物

2★ 关键一步:把 P、Q 换成两个互质的数

★★ 两行推导,问题就换了个样子

PQ 的最大公约数是 x₀ ⇒ 记 P = x₀·aQ = x₀·b,而且 gcd(a, b) = 1 (不然它们的公约数就比 x₀ 大了)。

再看最小公倍数:lcm(P, Q) = P·Q / gcd(P, Q) = x₀·a·b = y₀

a · b = y₀ / x₀,记作 n

⇒ 问题变成一句话:有多少个有序对 (a, b) 满足 a·b = ngcd(a, b) = 1 ⇒ 枚举 a√n 就够了,O(√n)。

p1029Div.cpp★ 第 ② 版:枚举 n 的因子对,判互质 —— O(√n),这一版就已经能过了
// P1029 第 ② 版:把问题缩到「a·b = n 且 gcd(a,b) = 1」,再枚举因子
//
// ============ ★ 关键一步(两行推导)============
//
// P、Q 的最大公约数是 x₀ ⇒ 记 P = x₀·a、Q = x₀·b,且 **gcd(a, b) = 1**。
// 而 lcm(P, Q) = P·Q / gcd(P, Q) = x₀·a·b = y₀
// ⇒ **a·b = y₀ / x₀**,记作 n。
//
// ⇒ 问题变成:**有多少个有序对 (a, b) 满足 a·b = n 且 gcd(a, b) = 1。**
// ⇒ 枚举 a 到 √n 即可,O(√n)。
//
// ⚠ 而第一件事是判 **y₀ % x₀ != 0 ⇒ 答案是 0**(gcd 一定整除 lcm)——
// 这正是[第 40 章第 7 步](/ch/40-gcd-euclid/)那半章坑的同类(见 p1029NoDiv.cpp)。
#include <bits/stdc++.h>
using namespace std;
static long long gcdll(long long a, long long b) { while (b) { long long t = a % b; a = b; b = t; } return a; }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long x0, y0;
if (!(cin >> x0 >> y0)) return 0;
if (y0 % x0 != 0) { cout << 0 << '\n'; return 0; } // ★ 少了这一句就是一整类错
long long n = y0 / x0, cnt = 0;
for (long long a = 1; a * a <= n; a++) {
if (n % a) continue;
long long b = n / a;
if (gcdll(a, b) != 1) continue;
cnt += (a == b) ? 1 : 2; // ⚠ a == b 只能算一次(只有 n = 1 时发生)
}
cout << cnt << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★★★ 再推一步:答案只可能是 2 的幂

★ 每个质因子只能整块地给一边

a·b = ngcd(a, b) = 1n每一个质因子的整块幂, 要么整个给 a,要么整个给 b(拆开给的话 ab 就有公因子了)。

⇒ 设 nω 个不同的质因子,则有序对 (a, b) 恰好有 2^ω 个。

  • 样例:n = 60/3 = 20 = 2²·5ω = 2 ⇒ 答案 4
  • n = 1(也就是 x₀ = y₀)⇒ ω = 0 ⇒ 答案 1(只有 P = Q = x₀

⇒ 于是只要质因数分解一次,连 gcd 都不用调。

p1029.cpp正解:答案 = 2^(n 的不同质因子个数)
// P1029 正解 —— 答案是 2 的「n 有几个不同质因子」次方
//
// ============ ★★★ 接着上一版再推一步 ============
//
// 要 a·b = n 且 gcd(a, b) = 1 ⇒ n 的**每一个质因子的整块幂**,
// 要么整个给 a,要么整个给 b(分开给的话 a、b 就有公因子了)。
// ⇒ 设 n 有 ω 个不同的质因子,则有序对 (a, b) 恰好有 **2^ω** 个。
//
// ★ 样例:x₀ = 3、y₀ = 60 ⇒ n = 20 = 2²·5 ⇒ ω = 2 ⇒ 答案 **4**。
// ★ n = 1(也就是 x₀ = y₀)⇒ ω = 0 ⇒ 答案 **1**(只有 P = Q = x₀ 这一种)。
//
// ⇒ 于是只要**质因数分解 n**,O(√n),而且连 gcd 都不用调。
// ⚠ 它和第 ② 版是同一个复杂度 —— 写它的理由不是快,是**它把答案的形状说清楚了**
// (见页面那张「答案只可能是 2 的幂」的表)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long x0, y0;
if (!(cin >> x0 >> y0)) return 0;
if (y0 % x0 != 0) { cout << 0 << '\n'; return 0; }
long long n = y0 / x0;
int omega = 0;
for (long long p = 2; p * p <= n; p++) {
if (n % p) continue;
omega++;
while (n % p == 0) n /= p;
}
if (n > 1) omega++; // 剩下的那个大质数
cout << (1LL << omega) << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 而这个公式不用「相信」—— 输入空间小,可以整个算一遍

第 18 章 P1002 那条:输入空间小的时候,「算一遍」比「对拍」又快又充分。 p1029Count.cppx₀, y₀ ∈ [2, 120]全部 14 161 组跑了一遍, 逐组拿 O(y₀²) 暴力核对:

量的是 实测
正解和暴力不一致的组数 0
有解(答案非 0)的组数 482
其中答案不是 2 的幂 0 组
这一段里最大的答案 8
题面范围(y₀ ≤ 10⁵)里最大的答案 64x₀ = 2y₀ = 60060 = 2·(2·3·5·7·11·13)

★ 最后一行顺带回答了「答案会不会很大」:n ≤ 5 × 10⁴ 最多塞得下 6 个不同质因子 (2·3·5·7·11·13 = 30030,再乘 17 就超了)⇒ 答案上界是 64int 绰绰有余。

4⚠⚠ 两个坑 —— 而其中一个,最顺手的生成器会把它整个盖住

p1029NoDiv.cpp✗ 坑一:忘了判「y₀ 不是 x₀ 的倍数 ⇒ 无解」
p1029Same.cpp✗ 坑二:a == b 那一次也算成了两个
★★★ 四个格子,「满足触发条件」和「真被抓」一个不差
档位(每档 300 轮) 无解(y₀ % x₀ ≠ 0)的轮数 坑一被抓 n = 1x₀ = y₀)的轮数 坑二被抓
★ 0 照题面随机 292 292 3 3
⚠⚠ 1 先挑 x₀、再挑倍数当 y₀ 0 精确的 0 14 14
2 专造 x₀ = y₀ 0 0 300 300

★★★ 第 ② 行才是这一节的主角:那是所有人都会顺手写出来的「贴心」生成器 (「先挑一个 x₀,再挑它的倍数当 y₀,这样保证有解嘛」)—— 而它把坑一打成精确的 0。 ⇒ ★★ 生成器「照顾」了输入的合法性,就等于把「程序自己该判合法性」这件事测没了。 ⇒ 和第 25 章那三次是同一个形状:生成器最自然的默认值,往往正是某个 bug 的藏身处。

★ 反过来,照题面随机那一档里 300 轮只有 8 组有解 —— ⚠ 也就是说,如果你用这一档,那 292 轮验的都是「两边一起输出 0」 (「一致有两种:都算对了,和都没算」)。 ⇒ 两档都要有:一档逼程序判合法性,一档才真的在测算法。

★ 坑二的触发线更窄:要 a == bgcd(a,a) = 1a = 1n = 1x₀ 恰好等于 y₀。 照题面随机撞上的概率约 1/200 ⇒ 那一档只抓到 3 次 (抓不到时别加轮数,去想那条线在哪儿)。

5★ 哪一版就已经能过了

p1029Count.cpp本页所有数字的出处(含全范围穷举那一节)
// P1029 的度量程序:./p1029Count csv (本页的数字都出自它)
//
// legal : ★★★ 照题面随机 (x₀, y₀),**有多少组是有解的**(y₀ % x₀ == 0)——
// 这一个数解释了「忘了判整除」那版为什么一抓一个准,
// 也解释了**顺手写的「贴心」生成器为什么把它打成精确的 0**。
// exh : ★★ 输入空间小的时候,「算一遍」比「对拍」又快又充分 ——
// 把 x₀, y₀ ≤ 120 的 **全部** 组合跑一遍,拿 O(y₀²) 暴力逐组核对 2^ω 那个公式。
// pow2 : ★ 顺带把答案的**形状**数出来:它只可能是 2 的幂,而且在题面范围里最大是多少。
// ms : 顶格上暴力和正解的毫秒。
//
// ⚠ 秒表用 steady_clock 在进程内量。
// ⚠ 这里**复刻**了 p1029Gen.cpp 的档位逻辑(同一个 mt19937、同一个种子公式)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static long long gcdll(long long a, long long b) { while (b) { long long t = a % b; a = b; b = t; } return a; }
/** 正解:2^ω(y₀/x₀),无解给 0 */
static long long solve(long long x0, long long y0) {
if (y0 % x0 != 0) return 0;
long long n = y0 / x0;
int w = 0;
for (long long p = 2; p * p <= n; p++) {
if (n % p) continue;
w++;
while (n % p == 0) n /= p;
}
if (n > 1) w++;
return 1LL << w;
}
/** 暴力:双重枚举 */
static long long brute(long long x0, long long y0) {
long long c = 0;
for (long long p = 1; p <= y0; p++)
for (long long q = 1; q <= y0; q++) {
long long g = gcdll(p, q);
if (g == x0 && p / g * q == y0) c++;
}
return c;
}
static void genPair(unsigned seed, int mode, long long& x0, long long& y0) {
mt19937 rng(seed * 1000003u + 20260901u);
if (mode == 0) { x0 = 2 + (long long)(rng() % 200u); y0 = 2 + (long long)(rng() % 200u); }
else if (mode == 1) { x0 = 2 + (long long)(rng() % 40u); long long k = 1 + (long long)(rng() % 20u); y0 = x0 * k; }
else { x0 = 2 + (long long)(rng() % 200u); y0 = x0; }
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 三个档位里「有解」的比例 */
int legal[3] = {0, 0, 0}, nIsOne[3] = {0, 0, 0};
for (int m = 0; m < 3; m++)
for (int s = 1; s <= 300; s++) {
long long x0, y0; genPair(s, m, x0, y0);
if (y0 % x0 == 0) { legal[m]++; if (y0 / x0 == 1) nIsOne[m]++; }
}
/* ② 全范围穷举:x₀, y₀ ≤ LIM 的全部组合 */
const int LIM = 120;
long long total = 0, mismatch = 0, nonZero = 0, notPow2 = 0, best = 0;
for (long long x0 = 2; x0 <= LIM; x0++)
for (long long y0 = 2; y0 <= LIM; y0++) {
total++;
long long a = solve(x0, y0), b = brute(x0, y0);
if (a != b) mismatch++;
if (a) { nonZero++; if (a & (a - 1)) notPow2++; if (a > best) best = a; }
}
/* ③ 题面范围里答案最大能到多少 */
long long bigBest = 0, bigX = 0, bigY = 0;
for (long long x0 = 2; x0 <= 4; x0++) // ⚠ 题面是 2 ≤ x₀,别从 1 起
for (long long n = 1; x0 * n <= 100000; n++) {
long long v = solve(x0, x0 * n);
if (v > bigBest) { bigBest = v; bigX = x0; bigY = x0 * n; }
}
/* ④ 秒表 */
// ⚠ 正解是亚毫秒级的 —— 单跑一次量到的是噪声(本书第 278 条那一跤)⇒ 跑 REP 次取平均
long long X = 2, Y = 100000;
const int REP = 20000;
long long r1 = 0;
auto t0 = steady_clock::now();
// ⚠ 每一次都必须真的走完质因数分解 —— 让 y₀ 恒为 x₀ 的倍数,
// 否则一半的调用在 `y0 % x0` 那一句就返回了,量到的是空壳(本书踩过好几次的那件事)。
long long acc = 0;
for (int i = 0; i < REP; i++) { r1 = solve(X, Y - 2 * (i % 100)); acc += r1; }
double usFast = duration<double, micro>(steady_clock::now() - t0).count() / REP;
if (acc == 0) usFast = -1; // ★ 自检:真跑过就不可能全是 0
long long X2 = 2, Y2 = 4000; // 暴力只能量到这个规模
t0 = steady_clock::now(); long long r2 = brute(X2, Y2);
double msBrute = duration<double, milli>(steady_clock::now() - t0).count();
double proj = msBrute * ((double)Y * Y) / ((double)Y2 * Y2) / 1000.0; // 外推到顶格(秒)
if (csv) {
for (int m = 0; m < 3; m++) printf("legal%d,%d\nnone%d,%d\n", m, legal[m], m, nIsOne[m]);
printf("exhTotal,%lld\nexhMismatch,%lld\nexhNonZero,%lld\nexhNotPow2,%lld\nexhBest,%lld\n",
total, mismatch, nonZero, notPow2, best);
printf("bigBest,%lld\nbigX,%lld\nbigY,%lld\n", bigBest, bigX, bigY);
printf("msFastUs,%.2f\nmsBrute,%.0f\nprojSec,%.0f\nans,%lld\nansB,%lld\n",
usFast, msBrute, proj, r1, r2);
return 0;
}
printf("① 照题面随机 (x₀, y₀) 300 轮,有解(y₀ %% x₀ == 0)的组数:\n");
const char* NM[3] = {"0 照题面随机", "1 ★ 先挑 x₀ 再挑倍数(顺手的「贴心」写法)", "2 专造 x₀ == y₀"};
for (int m = 0; m < 3; m++)
printf(" %-42s %3d / 300 其中 n = 1 的 %3d 组\n", NM[m], legal[m], nIsOne[m]);
printf("\n② 全范围穷举 x₀, y₀ ∈ [2, %d](共 %lld 组,逐组拿 O(y₀²) 暴力核对):\n", LIM, total);
printf(" 正解和暴力不一致的组数 = %lld\n", mismatch);
printf(" 答案非 0 的 %lld 组里,**不是 2 的幂**的有 %lld 组;这一段里最大答案 %lld\n",
nonZero, notPow2, best);
printf("\n③ 题面范围(y₀ ≤ 10⁵)里答案最大是 %lld(x₀ = %lld, y₀ = %lld)\n", bigBest, bigX, bigY);
printf("\n④ 秒表:正解在顶格 (%lld, %lld) 上 **%.1f 微秒**(跑 %d 次取平均 —— 亚毫秒的东西不能只跑一次);\n",
X, Y, usFast, REP);
printf(" 暴力在 (%lld, %lld) 上就要 %.0f 毫秒 ⇒ 按 y₀² 外推到顶格约 **%.0f 秒**\n",
X2, Y2, msBrute, proj);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
第 ② 版(枚举因子)就能过,正解只是把答案的形状说清楚了
版本 顶格 交上去
① 双重枚举 外推约 318 秒 0 分
★ ② 枚举 n 的因子对,判互质 O(√n),微秒级 AC
★ ③ 质因数分解,答案 = 2^ω 0.21 微秒(跑两万次取平均) AC

第 ② 版就够了 —— 第 ③ 版快出来的那点时间在这道题上一文不值, 它的价值是把答案的形状说清楚了(只可能是 2 的幂,上界 64), 而那才是这道题真正考的东西。

⚠ 最后一条和算法无关的:P·Q = x₀·y₀ 最大是 10¹⁰撑破 int —— 所以暴力那一版里写的是 p / g * q == y0先除后乘), 和第 40 章第 7 步那个 lcm 的手法一模一样。