0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1075,日期见页头。两边不一致时信原站。
题目描述
已知正整数 n 是两个不同的质数的乘积,试求出两者中较大的那个质数。
输入格式
输入一个正整数 n。
输出格式
输出一个正整数 p,即较大的那个质数。
数据范围
1 ≤ n ≤ 2 × 10⁹。
时限 1 秒,内存 128 MB。(题目来源:NOIP 2012 普及组 第一题)
输入输出样例
输入
21
输出
7
21 = 3 × 7 ⇒ 较大的那个是 7。
1第 ① 版:照定义硬做
// ✗ 第 ① 版:照定义硬做 —— 枚举 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;}点「运行 ▶」看结果
题目问「较大的那个质数」,那就把所有质因子都找出来取最大。它是对的 —— 档 0 / 档 1 的对拍都拿它当参照物(⚠ 顶格那一档它跑不动,那儿改用正解当参照物)。
⚠ 但顶格 n = 2 × 10⁹:光外层就 20 亿次,每次还要再判一遍质数。0 分。
2⚠ 第 ② 版:「这一章刚学了筛,那就筛」
筛法要为 [2, n] 里每一个数留一个格子。顶格 n = 2 × 10⁹:
一个 bool 一个数,要多少 |
1907 MB(约 1.86 GB) |
| 题面给多少 | 128 MB |
| 差多少 | ★ 14.9 倍 |
⇒ 这道题筛不动,只能试除。
★ 而这不是「筛法不好」,是筛法的开销跟着 n 走,而这道题的 n 大到没边 ——
同一章的 P3912 那个 n = 10⁸ 就正好卡在能筛和不能筛的分界线上。
(页面上这一份给筛的上限封了顶,超过 2 × 10⁷ 直接打印 MLE ——
那正是它在评测机上的下场,只是不至于把你的浏览器一起拖走。)
3★ 关键一步:只找最小的那个因子,另一个是白送的
n = p · q(p < q 都是质数):
① 较小的那个 p 一定不超过 √n。
不然 p > √n 且 q > p > √n ⇒ p·q > n,矛盾。
⇒ 从 2 往上试除,44 721 步之内一定停。
② 从 2 往上数,碰到的第一个因子一定是质数 —— 所以根本不用判质数。
它若还有一个更小的因子 d(d ≥ 2),那 d 也整除 n,
就轮不到它当「第一个」了。
⇒ 找到第一个因子 p,直接输出 n / p。一个 for 循环,四行。
// 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;}点「运行 ▶」看结果
把循环上界从 i * i <= n 换成最笨的 i <= n(照样 break),
拿 n ≤ 200 000 的全部合法输入逐个数试除次数:
| 量的是 | 实测 |
|---|---|
| 两种上界的试除次数不同的组数 | ★ 0 |
★ 道理就是上面那句①:最小质因子必 ≤ √n,循环总在那儿停 ——
上界写多大,那几步根本走不到。
⇒ i * i <= n 换来的不是速度,是「万一题面那句保证不成立,它也不会跑飞」。
⚠ 这是本书第 N 次「看着像 bug,其实一次都不会错」 (P2024、P1168、P1171)—— 「这样写会错」和「这样写会慢」,得分清楚。
4⚠ 三个错法 —— 而它们各自被什么挡住,三种答案都不一样
旋钮只有一个:题面那半句「两个不同的质数」。把它拧掉(允许 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 那条的又一次现场,而这次两个方向同时出现在一张表里)。
| 手段 | 对错法二(不 break)管不管用 |
|---|---|
官方样例 21 |
✗ 输出 7,一个字不差 |
| 三档对拍共 900 轮 | ✗ 一次都没抓到 |
| 为什么 | 它的答案永远是对的:n = p·q 在 [2, n) 里的因子只有 p 和 q,最后记下的就是 q |
挂它的是时间:
| 量的是 | 正解 | ✗ 不 break |
|---|---|---|
| 顶格最坏走多少次 | 44 710 | 1 999 999 998 |
| 倍数 | ★ 44 733 倍 | |
| 本机秒表 | 0.063 毫秒(跑一万次取平均) | n = 2 × 10⁸ 上 282 毫秒 ⇒ 顶格外推 ★ 2.8 秒 |
⇒ 时限 1 秒,它超了。而这件事样例和对拍都是聋的,只能数次数。
5★ 两笔三十秒的算术:int 够不够,生成器怎么造
// 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,而 i 最大就是那个最小质因子 p。
把题面允许的 p 全找一遍(要存在质数 q > p 使 p·q ≤ 2 × 10⁹):
| 量的是 | 实测 |
|---|---|
能出现的最大的 p |
44 711(配 q = 44 729,n = 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★ 哪一版就已经能过了
| 版本 | 顶格 | 交上去 |
|---|---|---|
| ① 枚举 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」—— 它在这儿不是一个优化,是唯一的解法来源。