题单 · 习题解析

洛谷 P1075 [NOIP 2012 普及组] 质因数分解

★★ 关键一步两句话:n = p·q ⇒ **较小的那个必 ≤ √n**,而**从 2 往上碰到的第一个因子一定是质数**(它若有更小的因子,那个也整除 n)⇒ 连判质数都不用,找到就输出 n/p;⚠ 顶格 n = 2×10⁹ ⇒ **筛不动**(一个字节一个数要 1907 MB,题面只给 128 ⇒ 差 **14.9 倍**);★★★ 而这一页最干净的是**一个旋钮两个 bug、两头都是精确的 0**:题面那半句「两个**不同**的质数」拧掉之后,「输出较小的那个」从 300 掉到 **0**(p == q 时两个答案是同一个数)、「i*i < n 少个等号」从 **0** 涨到 300 —— 同一档同时是两个「精确的 0」的自检;⚠ 而「不 break 扫到底」**答案永远对**(样例和 900 轮对拍全聋),挂在时间上:44 710 步 vs 20 亿步,**44 733 倍**,顶格外推 2.8 秒 / 时限 1 秒;★ 顺带两笔算术:`i*i` 用 int **恰好够、余量 7.4%**(最大 p = 44711),以及顺手随机的 n 只有 **19.0%** 真是「两个不同质数之积」⇒ 这道题的生成器**必须**反着造(⚠ 和上一章 P1029 那个「贴心生成器把考点测没了」正好相反 —— 判据是「合法性该谁负责」)

原题:洛谷 P1075出自 第 41 章 质数:试除 → 埃氏筛 → 线性筛 的题单题面本地存档:2026-09-02
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

已知正整数 n两个不同的质数的乘积,试求出两者中较大的那个质数。

输入格式

输入一个正整数 n

输出格式

输出一个正整数 p,即较大的那个质数。

数据范围

1 ≤ n ≤ 2 × 10⁹

时限 1 秒,内存 128 MB。(题目来源:NOIP 2012 普及组 第一题)

输入输出样例

输入

21

输出

7

21 = 3 × 7 ⇒ 较大的那个是 7

1第 ① 版:照定义硬做

p1075Brute.cpp第 ① 版:枚举 2..n,是因子又是质数就取最大 —— O(n√n),参照物
// ✗ 第 ① 版:照定义硬做 —— 枚举 2..n,是因子又是质数就更新最大值
//
// 这是绝大多数人真实的第一反应:题目问「较大的那个质数」,那就把所有质因子都找出来取最大。
// 它是对的,也是本页的**参照物**;但复杂度是 O(n√n),顶格 n = 2×10⁹ 想都不用想。
#include <bits/stdc++.h>
using namespace std;
static bool isPrime(int x) {
if (x < 2) return false;
for (int i = 2; (long long)i * i <= x; i++) if (x % i == 0) return false;
return true;
}
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
int ans = 1;
for (int i = 2; i <= n; i++)
if (n % i == 0 && isPrime(i)) ans = i;
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题目问「较大的那个质数」,那就把所有质因子都找出来取最大。它是对的 —— 档 0 / 档 1 的对拍都拿它当参照物(⚠ 顶格那一档它跑不动,那儿改用正解当参照物)。

⚠ 但顶格 n = 2 × 10⁹:光外层就 20 亿次,每次还要再判一遍质数。0 分。

2⚠ 第 ② 版:「这一章刚学了筛,那就筛」

p1075Sieve.cpp⚠ 第 ② 版:筛到 n 再从大往小找 —— 小数据能跑,顶格 MLE
⚠ 这道题是这一章的第一个「筛不动」——而它是一句乘法

筛法要为 [2, n]每一个数留一个格子。顶格 n = 2 × 10⁹

一个 bool 一个数,要多少 1907 MB(约 1.86 GB)
题面给多少 128 MB
差多少 14.9 倍

这道题筛不动,只能试除。 ★ 而这不是「筛法不好」,是筛法的开销跟着 n 走,而这道题的 n 大到没边 —— 同一章的 P3912 那个 n = 10⁸ 就正好卡在能筛和不能筛的分界线上。

(页面上这一份给筛的上限封了顶,超过 2 × 10⁷ 直接打印 MLE —— 那正是它在评测机上的下场,只是不至于把你的浏览器一起拖走。)

3★ 关键一步:只找最小的那个因子,另一个是白送的

★★ 两句话,复杂度就从 O(n√n) 掉到 O(√n)

n = p · qp < q 都是质数):

① 较小的那个 p 一定不超过 √n 不然 p > √nq > p > √np·q > n,矛盾。 ⇒ 从 2 往上试除,44 721 步之内一定停

② 从 2 往上数,碰到的第一个因子一定是质数 —— 所以根本不用判质数。 它若还有一个更小的因子 dd ≥ 2),那 d 也整除 n, 就轮不到它当「第一个」了。

⇒ 找到第一个因子 p,直接输出 n / p一个 for 循环,四行。

p1075.cpp★ 正解:试除到 √n,答案 = n / 最小质因子 —— 这一版就已经能过了
// P1075 质因数分解 —— 正解:试除到 √n,答案 = n / 最小质因子
//
// ★ 两句话就是全部:
// ① n = p·q(p < q 都是质数)⇒ **较小的那个 p ≤ √n**(不然 p·q > n);
// ② 从 2 往上数,**碰到的第一个因子一定是质数** ——
// 它若有更小的因子 d,那 d 也整除 n,就轮不到它当「第一个」了。
// ⇒ 于是根本不用判质数,找到第一个因子就停,输出 n / 它。
//
// ⚠ 循环上界写 i * i <= n:顶格 n = 2×10⁹ 时 i 最大到 44721,
// i * i 最大约 2×10⁹ < 2³¹−1 = 2 147 483 647 ⇒ int **恰好够,余量 7.4%**
// (p1075Count.cpp 把这个数算出来了)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
for (int i = 2; (long long)i * i <= n; i++)
if (n % i == 0) { printf("%d\n", n / i); return 0; }
printf("%d\n", n); // 题面保证不会走到这里(n 一定是两个质数之积)
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 而「试到 √n 就够」这句话,在这道题上省的不是时间

把循环上界从 i * i <= n 换成最笨的 i <= n(照样 break), 拿 n ≤ 200 000 的全部合法输入逐个数试除次数:

量的是 实测
两种上界的试除次数不同的组数 0

★ 道理就是上面那句①:最小质因子必 ≤ √n,循环总在那儿停 —— 上界写多大,那几步根本走不到。 ⇒ i * i <= n 换来的不是速度,是「万一题面那句保证不成立,它也不会跑飞」。

⚠ 这是本书第 N 次「看着像 bug,其实一次都不会错」 (P2024P1168P1171)—— 「这样写会错」和「这样写会慢」,得分清楚。

4⚠ 三个错法 —— 而它们各自被什么挡住,三种答案都不一样

p1075Small.cpp✗ 错法一:输出了较小的那个
p1075NoBreak.cpp✗ 错法二:不 break,一路扫到 n 取最后一个因子 —— 答案永远对
p1075Lt.cpp✗ 错法三:上界写成 i * i < n(少了一个等号)
★★★ 一个旋钮,两个 bug,两头都是精确的 0 —— 本书量到的最干净的一次

旋钮只有一个:题面那半句「两个不同的质数」。把它拧掉(允许 p == q,也就是 n = p²):

档位(每档 300 轮) ✗ 输出较小的 i * i < n
★ 0 照题面:p ≠ q 300 / 300 精确的 0
⚠⚠ 1 违反题面:p == q 精确的 0 300 / 300
★ 2 顶格(p·q 顶到 2 × 10⁹ 300 / 300 精确的 0

两行完全互补,而且两个 0 都能证:

  • p == q 时,「较大的」和「较小的」是同一个数 ⇒ 错法一无从错起;
  • p ≠ q 时,p·p < p·q = n ⇒ 那个 < 照样能走到 i = p ⇒ 错法三无从错起。

⇒ ★★ 这一档同时干了两件事(和 P1439 那次同形): 给「精确的 0」做自检称出那半句约束是谁的命门 —— 它是错法三的命门,对错法一却是反着的保护伞。 ⇒ ★★★ 「这句约束重不重要」不是题目的属性,是「题目 × 你写的那一版」的属性第 28 章 P1171 那条的又一次现场,而这次两个方向同时出现在一张表里)。

p1075Gen.cpp生成器(三档:照题面 / 违反题面 / 顶格)
⚠ 而错法二躲过了上面「所有」手段
手段 对错法二(不 break)管不管用
官方样例 21 ✗ 输出 7,一个字不差
三档对拍共 900 轮 一次都没抓到
为什么 它的答案永远是对的n = p·q[2, n) 里的因子只有 pq,最后记下的就是 q

挂它的是时间

量的是 正解 ✗ 不 break
顶格最坏走多少次 44 710 1 999 999 998
倍数 44 733 倍
本机秒表 0.063 毫秒(跑一万次取平均) n = 2 × 10⁸ 上 282 毫秒 ⇒ 顶格外推 ★ 2.8 秒

⇒ 时限 1 秒,它超了。而这件事样例和对拍都是聋的,只能数次数

5★ 两笔三十秒的算术:int 够不够,生成器怎么造

p1075Count.cpp本页所有数字的出处
// P1075 的度量程序:./p1075Count csv (本页的数字都出自它)
//
// exh : ★★ 全范围穷举 —— n ≤ 10⁷ 的**全部**「两个不同质数之积」,
// 拿一条完全无关的路(线性筛出来的最小质因子表)逐个核对正解。
// same : ★★★ 「循环上界写 i <= n」和「写 i * i <= n」**试除次数逐个相同** ——
// 因为最小质因子必 ≤ √n,循环总在那儿停 ⇒ **上界写多大在这道题上是免费的**。
// int : ★ 「i * i 会不会撑破 int」是一道算术题:答案是**恰好够**,余量多少。
// steps : ★ 正解最坏走多少次 ↔ 「不 break 扫到底」走多少次。
// ms : 秒表(正解跑一万次取平均;不 break 那版在 n = 2×10⁸ 上量一次再外推顶格)。
// sieve : ⚠ 「那就筛」要多少内存。
// dens : ⚠ 顺手随机一个 n,有多大概率真是「两个不同质数之积」——
// 这道题的生成器**必须**照题面反着造。
//
// ⚠ 秒表用 steady_clock 在进程内量([第 35 章那一跤](/sol/p2866/):别拿 shell 的时间戳相减)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static const int EXH = 10000000; // 10⁷
static const long long NMAX = 2000000000LL; // 题面上界
/** 正解:试除到 √n,返回答案;trials 带回试除了几次 */
static int solve(int n, long long& trials) {
trials = 0;
for (int i = 2; (long long)i * i <= n; i++) {
trials++;
if (n % i == 0) return n / i;
}
return n;
}
/** 上界写 i <= n 的版本(同样 break):只数次数 */
static long long trialsLoose(int n) {
long long t = 0;
for (int i = 2; i <= n; i++) { t++; if (n % i == 0) break; }
return t;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 线性筛出 [2, 10⁷] 的最小质因子 —— 这是「完全无关的另一条路」 */
vector<int> minp(EXH + 1, 0);
vector<int> pr;
pr.reserve(700000);
for (int i = 2; i <= EXH; i++) {
if (!minp[i]) { minp[i] = i; pr.push_back(i); }
for (int p : pr) {
if ((long long)p * i > EXH || p > minp[i]) break;
minp[(long long)p * i] = p;
}
}
/* ② 全范围穷举:所有 n = p·q(p < q 都是质数)*/
long long exhTotal = 0, exhMismatch = 0, sameDiff = 0, semiAll = 0;
for (int n = 4; n <= EXH; n++) {
int p = minp[n];
int m = n / p;
if (minp[m] != m) continue; // m 不是质数 ⇒ n 不是两个质数之积
semiAll++;
if (p == m) continue; // 题面要「两个**不同**的质数」
exhTotal++;
long long tr = 0;
if (solve(n, tr) != m) exhMismatch++;
if (n <= 200000 && trialsLoose(n) != tr) sameDiff++; // 次数对照只跑前一段(够说明问题)
}
/* ③ 「i * i 撑不撑得破 int」:找出最大的、能出现在题面里的最小质因子 */
long long maxP = 0, maxQ = 0;
{
// p 是较小的那个 ⇒ p ≤ √(2×10⁹);要求存在质数 q > p 使 p·q ≤ 2×10⁹
int lim = 44730;
vector<char> comp(lim + 1, 0);
vector<int> sp;
for (int i = 2; i <= lim; i++) {
if (!comp[i]) sp.push_back(i);
for (long long j = (long long)i * i; j <= lim; j += i) comp[j] = 1;
}
for (size_t i = 0; i + 1 < sp.size(); i++) {
long long p = sp[i], q = sp[i + 1];
if (p * q <= NMAX) { maxP = p; maxQ = q; }
}
}
long long maxSq = maxP * maxP;
long long INT_TOP = 2147483647LL;
/* ④ 秒表 */
int topN = (int)(maxP * maxQ);
double usFast = 0;
{
auto t0 = steady_clock::now();
volatile long long acc = 0;
long long tr = 0;
for (int r = 0; r < 10000; r++) acc = acc + solve(topN, tr);
auto t1 = steady_clock::now();
usFast = duration<double, micro>(t1 - t0).count() / 10000.0;
}
double msNoBreak = 0;
{
const int probe = 200000000; // 2×10⁸,再外推到顶格
auto t0 = steady_clock::now();
volatile int ans = probe;
for (int i = 2; i < probe; i++) if (probe % i == 0) ans = i;
(void)ans;
auto t1 = steady_clock::now();
msNoBreak = duration<double, milli>(t1 - t0).count();
}
double projNoBreakSec = msNoBreak / 1000.0 * (double)NMAX / 200000000.0;
/* ⑤ 「那就筛」要多少内存 */
double sieveMB = (double)NMAX / 1048576.0; // 一个字节一个数
double limitMB = 128.0;
if (csv) {
printf("exhTotal,%lld\n", exhTotal);
printf("exhMismatch,%lld\n", exhMismatch);
printf("semiAll,%lld\n", semiAll);
printf("sameDiff,%lld\n", sameDiff);
printf("maxP,%lld\n", maxP);
printf("maxQ,%lld\n", maxQ);
printf("maxSq,%lld\n", maxSq);
printf("intTop,%lld\n", INT_TOP);
printf("marginPct,%.1f\n", (double)(INT_TOP - maxSq) * 100.0 / (double)maxSq);
printf("worstTrial,%lld\n", maxP - 1);
printf("noBreakSteps,%lld\n", NMAX - 2);
printf("stepRatio,%.0f\n", (double)(NMAX - 2) / (double)(maxP - 1));
printf("usFast,%.3f\n", usFast);
printf("msNoBreak2e8,%.0f\n", msNoBreak);
printf("projNoBreakSec,%.1f\n", projNoBreakSec);
printf("sieveMB,%.0f\n", sieveMB);
printf("sieveRatio,%.1f\n", sieveMB / limitMB);
printf("densPct,%.1f\n", (double)exhTotal * 100.0 / (double)EXH);
return 0;
}
printf("① 全范围穷举(n ≤ 10⁷ 的全部「两个不同质数之积」)\n");
printf(" 这样的 n 一共 %lld 个(占 %.1f%%);正解和「线性筛的最小质因子表」不一致 %lld 组\n",
exhTotal, (double)exhTotal * 100.0 / EXH, exhMismatch);
printf(" ⚠ 顺手随机一个 n,只有 %.1f%% 真的是题面说的那种 ⇒ 生成器必须反着造\n",
(double)exhTotal * 100.0 / EXH);
printf("\n② 上界写 i <= n ↔ 写 i*i <= n:试除次数不同的组数 = %lld\n", sameDiff);
printf(" ⇒ 最小质因子必 ≤ √n,循环总在那儿停 —— 上界写多大在这道题上是免费的\n");
printf("\n③ int 够不够:题面里可能出现的最大「较小质数」= %lld(配 q = %lld)\n", maxP, maxQ);
printf(" ⇒ i*i 最大 %lld,而 int 上限 %lld ⇒ **恰好够,余量 %.1f%%**\n",
maxSq, INT_TOP, (double)(INT_TOP - maxSq) * 100.0 / (double)maxSq);
printf("\n④ 次数:正解最坏 %lld 次;不 break 扫到底 %lld 次 ⇒ 差 %.0f 倍\n",
maxP - 1, NMAX - 2, (double)(NMAX - 2) / (double)(maxP - 1));
printf(" 秒表:正解 %.3f 微秒(跑一万次取平均);不 break 在 n = 2×10⁸ 上 %.0f 毫秒 ⇒ 顶格外推 %.1f 秒\n",
usFast, msNoBreak, projNoBreakSec);
printf("\n⑤ 「那就筛」:顶格要 %.0f MB,而题面给 128 MB ⇒ 差 %.1f 倍\n", sieveMB, sieveMB / limitMB);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 一、`i * i` 会不会撑破 int —— 答案是「恰好够,余量 7.4%」

循环最后一次算的是 i * i,而 i 最大就是那个最小质因子 p。 把题面允许的 p 全找一遍(要存在质数 q > p 使 p·q ≤ 2 × 10⁹):

量的是 实测
能出现的最大的 p 44 711(配 q = 44 729n = 1 999 878 319
i * i 最大 1 999 073 521
int 上限 2 147 483 647
余量 7.4%

⇒ 和 P4779(6.1%)、P1462(4.0%)一样: 「恰好够」是出题人挑出来的,不是巧合 —— 题面那个 2 × 10⁹ 本身就贴着 2³¹−1 写的。 ★ 本页正解仍然写了 (long long)i * i代价是零,而它把这段推理从「必须成立」降成「不必读」

⚠ 二、这道题的生成器「必须」照题面反着造
量的是 实测
n ≤ 10⁷ 里,真是「两个不同质数之积」的 1 903 878
占比 19.0%

⇒ 顺手 rng() % 2000000000 造出来的 n五个里有四个题目根本不会给 —— 而在那些输入上,正解输出的 n / p 压根不是质数,「谁对谁错」这句话没有定义。 ⇒ 「生成器要亲手做到题面那几句保证」: 这道题只能先挑两个质数再乘起来

★ ⚠ 而这和上一章 P1029 正好构成一对,结论相反: 那道题「先挑 x₀ 再挑它的倍数」的贴心生成器把「忘了判合法性」测没了; 这道题题面自己保证了合法性 ⇒ 生成器非贴心不可。 ⇒ ★★ 判据不是「贴不贴心」,是「这件事到底该谁负责」 —— 题面写了保证,就是出题人负责;题面没写,就是你的程序负责。

6★ 哪一版就已经能过了

第 ③ 版(试除到 √n)就是正解,前面两版一分都拿不到
版本 顶格 交上去
① 枚举 2..n 判质数取最大 O(n√n) 0 分
② 先筛再找 1907 MB / 限制 128 MB MLE
✗ 不 break 扫到底 外推 2.8 秒 / 时限 1 秒 TLE(⚠ 而答案全对)
★ ③ 试除到 √n,输出 n / p 0.063 毫秒 AC

⇒ 这道题的全部内容就是第 41 章第 3 步那句「为什么只要试到 √x」—— 它在这儿不是一个优化,是唯一的解法来源。