正整数只有两类:除 1 和自身外没有别的因子的叫质数,有的叫合数。这个划分之所以重要,是因为唯一分解定理:任何大于 1 的整数都能写成若干质数之积,写法唯一。所以数论题的第一步几乎总是“把这个数拆成质数”,而拆之前必须先知道哪些数是质数。
| 要做的事 | 数据规模 | 用什么 | 复杂度 |
|---|---|---|---|
| 判单个 n 是否质数 | n ≤ 10^12 | 试除到 √n | ≤ 10^6 次除法 |
| 求 2..n 的全部质数 | n ≤ 10^7 | 埃氏筛 | O(n log log n) |
| 同上,还要最小质因子 | n ≤ 10^7 | 线性筛 | O(n) |
| 分解一个 n | n ≤ 10^12 | 试除到 √n | ≤ 10^6 次除法 |
| [L, R] 内的质数 | R ≤ 10^12,R−L ≤ 10^6 | 区间筛 | O(√R + (R−L)·log log R) |
先证明第一条结论,它决定了后面所有循环的边界。若 n = a·b 且 a ≤ b,则 a·a ≤ a·b = n,所以 a ≤ √n。也就是说:n 只要有真因子,就一定有一个不超过 √n。于是“试完 2..√n 都没有因子”与“n 是质数”完全等价。n = 10^12 时这是 10^6 次除法对 10^12 次除法——六个数量级的差距,就在这一个不等号上。
这一节的两个基础工具都很短,但每一行都有理由。先把结构看清楚,之后所有代码都是它们的变体。
| 组成 | 含义 / 格式要求 |
|---|---|
n | 要判断的数。用 long long:n 到 10^12 时 int 装不下。 |
if (n < 2) return false; | 0 和 1 不是质数。这是最常漏的一句,漏了会让答案多算一个 1。 |
n % 2 == 0 | 单独处理 2,之后就能每次跳过所有偶数,试除次数直接减半。 |
i <= n / i | 循环条件,等价于 i·i ≤ n。写成 i < n 会慢 √n 倍;写成 i·i ≤ n 在 i 用 int 时会溢出。 |
i += 2 | 只试奇数。3、5、7、9、…,其中 9、15 是合数,多试几次但省掉了全部偶数。 |
return false | 找到一个真因子就立刻退出,不必再试。 |
bool isPrime(long long n) {
if (n < 2) return false; // 0、1 不是质数
if (n % 2 == 0) return n == 2; // 唯一的偶质数是 2
for (long long i = 3; i <= n / i; i += 2) { // 只试奇数,且防止 i*i 溢出
if (n % i == 0) return false;
}
return true;
}i <= n / i 与 i * i <= n 等价,但前者永不溢出。这是数论里最该背下来的写法之一。
n 到 2×10^9 时最多试 22360 个奇数,一次判素不到一毫秒;但要判 10^5 个数就是 2.2×10^9 次,必须换筛法。
| 组成 | 含义 / 格式要求 |
|---|---|
isp[k] | 1 表示 k 还没被划掉、是质数;0 表示合数。用 char 不用 int:n = 10^7 时 10 MB 对 40 MB。 |
isp[0] = isp[1] = 0; | 筛前手动把 0 和 1 划掉。全局数组默认全 0,所以用 vector 构造时给初值 1 更省事。 |
i 只到 √n(优化一) | 合数 k 一定有一个 ≤ √k 的质因子,所以所有合数都会被不超过 √n 的质数划掉。i 超过 √n 后,内层循环一次也不会执行。 |
j 从 i·i 起(优化二) | i 的 2i、3i、…、(i−1)i 已经分别被 2、3、…、i−1 划掉了,从 i·i 起才第一次需要自己动手。 |
j += i | 步长就是 i,把 i 的所有倍数依次标 0。 |
复杂度 | O(n log log n)。n = 10^7 约 0.06 秒,n = 10^8 约 0.8 秒,n = 10^9 就得换策略了。 |
int n; cin >> n;
vector<char> isp(n + 1, 1); // 先假设 2..n 全是质数
isp[0] = isp[1] = 0; // 0、1 不是质数
for (int i = 2; i <= n / i; ++i) { // 优化一:外层只到 √n
if (!isp[i]) continue; // i 是合数,它的倍数早被更小的质数划过了
for (int j = i * i; j <= n; j += i) // 优化二:从 i*i 起
isp[j] = 0;
}
// 此时 isp[k] == 1 的 k 就是质数内层写成 j = 2 * i 答案仍然正确,只是多做一倍无用功。两个优化作用不同:优化一决定外层跑几轮,优化二决定内层从哪起步,谁也替代不了谁。
重复执行筛法时上一轮的表会残留,多测要重新 fill 一遍,或者把筛法写成函数、每次重新构造 vector。
小 A 有一个“质数口袋”,他从 2 开始依次检查每个自然数,是质数就装进口袋。口袋的负荷是里面所有质数之和,负荷不允许超过 L。
求口袋最多能装下多少个质数,以及此时口袋的负荷。
输入:一个正整数 L(1 ≤ L ≤ 105)。
输出:两行,第一行为最多能装的质数个数,第二行为此时的负荷。
题目要“装得尽量多”。所有质数都是正整数,所以装 2 一定比装 3 划算:要数量最多,就只能从最小的质数开始一个个往后装,装到再加一个就超重为止。这是唯一的最优策略,不需要搜索也不需要贪心证明。
剩下的问题是“2..L 里哪些是质数”。L ≤ 105,逐个试除要 105 × 316 ≈ 3×107 次,勉强能过;但要检查的是一整段连续区间,这正是筛法的场合——一次 O(L log log L) 把整张表标完。
两段拼起来:筛 2..L → 从 2 扫到 L,是质数就试着装。扫到第一个“装进去就超重”的质数时立刻停,因为它后面更大的质数只会更超重。
筛表:
isp[0] = isp[1] = 0; 其余 0..L 全置 1
i 从 2 到 L/i:若 isp[i],把 i*i, i*i+i, i*i+2i, ... 标 0
装口袋:
sum = 0, cnt = 0
i 从 2 到 L:
若 isp[i] 为 0,跳过
若 sum + i > L,break
否则 sum += i, cnt += 1两处容易写错的地方:用 break 还是 continue——超重必须 break,因为后面每个质数都更大,continue 会白扫到底;sum + i 的比较——用 sum + i > L 而不是 sum >= L,后者会把正好装满的最后一个质数丢掉。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005; // L ≤ 10^5,多留几个位置
bool isp[MAXN]; // isp[i] = true 表示 i 是质数
int main() {
int L;
cin >> L;
// ---------- ① 埃氏筛:标出 2..L 的所有质数 ----------
fill(isp, isp + L + 1, true); // 只填 0..L,别填满 MAXN
isp[0] = isp[1] = false; // 0 和 1 不是质数,漏了答案会多算一个
for (int i = 2; i <= L / i; ++i) { // 外层只到 √L:更大的 i 划不动任何新数
if (!isp[i]) continue; // i 自己是合数,跳过
for (int j = i * i; j <= L; j += i) {
isp[j] = false; // 从 i*i 起划,2i..(i-1)i 已被更小的质数划掉
}
}
// ---------- ② 从小到大装 ----------
int cnt = 0, sum = 0;
for (int i = 2; i <= L; ++i) {
if (!isp[i]) continue; // 不是质数
if (sum + i > L) break; // 再加就超重;后面的质数更大,一定也超
sum += i;
++cnt;
}
cout << cnt << "\n" << sum << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
const int MAXN = 100005; | L 最大 10^5,下标要能取到 10^5,所以开 100005 而不是 100000。 |
bool isp[MAXN]; | 全局数组默认全 false。所以下面必须先 fill 成 true——“全局数组默认是质数”是错觉。 |
fill(isp, isp + L + 1, true); | 只填 [0, L]。填满整个数组也不会错,但白刷 10 万个位置;写 L+1 表达的正是“只关心 0..L”。 |
isp[0] = isp[1] = false; | 0 和 1 不是质数。漏这句,1 会被当成质数装进口袋,cnt 多 1、sum 多 1。 |
for (int i = 2; i <= L / i; ++i) | 外层只到 √L。理由就是开头那个不等式:任何合数都有不超过 √n 的质因子。 |
if (!isp[i]) continue; | i 已被更小的质数划掉,它是合数,它的倍数必然也被划过了,跳过。 |
for (int j = i * i; j <= L; j += i) | 两个优化同时出现:从 i*i 起、以 i 为步长。 |
if (sum + i > L) break; | 超重就停。用 break 不用 continue:后面的质数更大,只会更超重。 |
sum += i; ++cnt; | 装进去,两个计数器同步更新。 |
cout << cnt << "\n" << sum << "\n"; | 两行输出,顺序是“数量、负荷”,题目规定的顺序。 |
埃氏筛有个小浪费:一个合数会被它的每个质因子各划一次,比如 30 会被 2、3、5 各划一遍。线性筛把它修掉——让每个合数只被它的最小质因子划一次,代价是多用一个数组 primes 存已找到的质数,顺手还能免费得到每个数的最小质因子表 lpf。
const int MAXN = 1000000 + 5;
vector<int> primes; // 已找到的质数,从小到大
int lpf[MAXN]; // lpf[k]:k 的最小质因子
bool isp[MAXN]; // 是否是质数
void eulerSieve(int n) {
primes.clear(); // 多测:上一轮攒下的质数必须清掉
fill(isp, isp + n + 1, true);
isp[0] = isp[1] = false;
for (int i = 2; i <= n; ++i) {
if (isp[i]) {
primes.push_back(i);
lpf[i] = i; // 质数的最小质因子是它自己
}
for (int p : primes) {
if ((long long)p * i > n) break; // 越界,后面的 p 只会更大
isp[p * i] = false;
lpf[p * i] = p; // p 是 p*i 的最小质因子
if (i % p == 0) break; // ★ 关键的一句,见下表
}
}
}| 代码片段 | 含义与理由 |
|---|---|
for (int p : primes) | 拿已知的质数从小到大去乘 i。乘出来的 p·i 是合数,顺手标掉。 |
if ((long long)p * i > n) break; | p·i 越界就跳出。p 递增,后面的只会更大,没有继续的必要。 |
lpf[p * i] = p; | 能执行到这一句,说明 p 不大于 i 的最小质因子,因此 p 一定是 p·i 的最小质因子。 |
if (i % p == 0) break; | ★ i 已含质因子 p。再乘更大的 p′,p′·i 的最小质因子仍是 p,应该由 p 去标,不在这里标——否则同一个合数被标两次,退化回埃氏筛。 |
每个合数只被标一次 | 任一合数 k 只会在 i = k / lpf[k]、p = lpf[k] 这一种情况下被标,所以内层总执行次数 ≤ n。 |
副产物 lpf | 有了 lpf,分解质因数从 O(√n) 降到 O(log n):反复除以 lpf 即可。后面算约数个数、欧拉函数,第一步都是这张表。 |
分解质因数就是试除法的直接应用,只有一个新要点:除干净之后,剩下的那部分不用再试。因为每除一次 n 就变小,循环条件 p ≤ n/p 会自动收紧;而循环结束时若 n 仍大于 1,这个 n 一定是质数(否则它会有个不超过 √n 的因子,早就被除走了)。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n;
cin >> n;
cout << n << " = ";
bool first = true;
for (long long p = 2; p <= n / p; ++p) { // 上界跟着 n 一起缩小
if (n % p != 0) continue; // p 不是因子
int c = 0;
while (n % p == 0) { n /= p; ++c; } // 除干净,c 就是指数
if (!first) cout << " * ";
cout << p << "^" << c;
first = false;
}
if (n > 1) { // 剩下的一定是质数
if (!first) cout << " * ";
cout << n << "^1";
}
cout << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
for (long long p = 2; p <= n / p; ++p) | 上界不是 √原数,而是 √当前 n。每除掉一个因子,n 变小,上界随之变小,循环会更早结束——这是复杂度能接受的根本原因。 |
while (n % p == 0) { n /= p; ++c; } | 一次除干净。换成 if 只除一次,n 就没被除透:360 会拆成 2^1 * 3^1 * 4^1 * 15^1,合数 4 和 15 都被当成了质因子。 |
if (n > 1) 输出 n^1 | 循环结束后 n 若大于 1,它必是质数。例 n = 14:p = 2 除完 n 变成 7,此时条件 3 ≤ 7/3 = 2 已不成立,循环立即结束,剩下的 7 只能在这里补输出。 |
p <= n / p | 写成 p * p <= n,当 n 到 10^18 时 p 接近 10^9,p*p 溢出成负数,循环变成死循环,表现为 TLE。 |
bool first | 只控制分隔符,保证行末不多一个 “ * ”。格式题里多一个字符就是 WA。 |
最后一招:R 可以很大(1012),但区间长度 R−L 很小(106)。筛法筛不了 1012 个位置,可我们只需要 [L, R] 这一小段。做法是:先筛出 √R ≤ 106 以内的质数,再用这些质数去划 [L, R] 里的倍数,下标整体平移 L,用一个长度 R−L+1 的数组装下。
// 统计 [L, R] 内的质数个数,要求 L ≥ 1,R ≤ 10^12,R - L ≤ 10^6
long long countPrimes(long long L, long long R) {
int lim = (int)sqrt((long double)R) + 1; // 只需 √R 以内的质数
vector<char> small(lim + 1, 1);
small[0] = small[1] = 0;
for (int i = 2; i <= lim / i; ++i)
if (small[i])
for (int j = i * i; j <= lim; j += i) small[j] = 0;
vector<char> seg(R - L + 1, 1); // seg[k] 对应数字 L + k
if (L == 1) seg[0] = 0; // 1 不是质数
for (int i = 2; i <= lim; ++i) {
if (!small[i]) continue;
long long p = i;
// 从 p*p 与 “不小于 L 的最小 p 的倍数” 中取较大者
long long start = max(p * p, (L + p - 1) / p * p);
for (long long j = start; j <= R; j += p) seg[j - L] = 0;
}
long long cnt = 0;
for (long long k = L; k <= R; ++k) cnt += seg[k - L];
return cnt;
}| 代码片段 | 含义与理由 |
|---|---|
int lim = sqrt((long double)R) + 1; | sqrt 返回浮点数,可能比真值差一点点,加 1 保证上界够用。多筛一格没有代价。 |
(L + p - 1) / p * p | 向上取整到 p 的倍数:不小于 L 的最小 p 的倍数。整数除法自动下取整,所以先加 p−1。 |
max(p * p, ...) | ★ 不能只写后面那半。若 L ≤ p,向上取整会得到 p 自己,于是质数 p 被自己划掉,答案少算。取 max 保证 p 本身不被划。 |
seg[j - L] = 0; | 下标平移。所有真实数字都减去 L,落到 [0, R−L] 这个小区间里。 |
if (L == 1) seg[0] = 0; | 区间左端可能是 1,单独处理。L ≥ 2 时这一行不会误伤。 |
WA,答案多 1。—— 筛完忘了 isp[1] = false。1 被当成质数先装进去,白占 1 点负荷,后面的数整体错位(L = 100 时第二行变成 78,而不是 100)。
TLE,卡在 n 接近 231 的数据点上。—— 循环写成 i * i <= n 且 i 是 int。i 到 46341 时 i·i = 2147488281 > INT_MAX,溢出成负数,条件永远成立,循环停不下来。改用 i <= n / i。
WA,表里全是 true、cnt 大得离谱。—— 把 bool isp[] 换成 int isp[] 后还用 memset(isp, 1, sizeof(isp))。逐字节填 1 得到 16843009,不是 1(第 26 节的坑在这里复发)。
RE,越界写。—— 数组开 MAXN = 1000000 却让 n = 106,isp[1000000] 已经越界一格,读到数组外的内存,症状是随机 WA 或 RE。开 1000005。
WA,代码看着全对但答案是错的。—— 分解质因数里把 while (n % p == 0) 写成 if,同一个质因子只除一次,n 没除透:360 会拆成 2^1 * 3^1 * 4^1 * 15^1,合数被当成了质因子。
TLE,n 是大质数时跑不出来。—— 分解循环写成 p < n,对 n = 999999937 要试 109 次。必须是 p <= n / p。
WA,区间筛少算质数。—— 起手写成 start = (L + p - 1) / p * p,漏了 max(p * p, ...),于是区间内的质数 p 自己被自己划掉。症状是答案比正确值少一点,且 L 越小区越多。
关掉页面,从空文件把 P5723 敲一遍并提交到洛谷。
改一改 ①:把外层换成 for (int i = 2; i <= L; ++i),输入 100000,用 time ./a.out 计时,和原版比一下——两行代码差多少倍?
改一改 ②:把内层的 j = i * i 改成 j = 2 * i,重跑样例与 100000。答案变了吗?为什么变不了(想清楚“多做的功”和“错的功”的区别)?
改一改 ③:把 bool isp[MAXN] 改成 int isp[MAXN],把 fill(...) 换成 memset(isp, 1, sizeof(isp)),打印 isp[4] 的值。看清楚它是不是 1,再解释为什么筛完表里还有合数被当成质数。
改一改 ④:写一份分解质因数的程序,输入 999999937(109 以内最大的质数),先把循环写成 p < n 计时,再改成 p <= n / p 计时。
判 n 以内每个数是不是质数,逐个试除是 O(n√n):n = 106 时 √n = 103,总工作量 109,必然超时。筛法把这件事压到 O(n log log n),差在哪?
筛法的工作量:质数 p 会划掉 n/p 个倍数,总工作量 = Σp ≤ n n/p = n · Σp ≤ n 1/p。而质数倒数和满足 Σp ≤ n 1/p ≈ ln ln n + 0.2615(梅滕斯定理)。
| n | ln ln n | n log log n 的常数因子 |
|---|---|---|
| 10^5 | 2.44 | 2.4 |
| 10^7 | 2.78 | 2.8 |
| 10^9 | 3.03 | 3.0 |
| 10^12 | 3.32 | 3.3 |
这张表的意思是:n 从 105 涨到 1012(一千万倍),常数因子只从 2.4 涨到 3.3。所以埃氏筛几乎就是线性的,重活全在 n 那个乘法里。
素数的个数:π(n) ≈ n / ln n,实际值比估计略大。
| n | π(n) 实际值 | n / ln n |
|---|---|---|
| 10^5 | 9592 | 8686 |
| 10^6 | 78498 | 72382 |
| 10^7 | 664579 | 620421 |
| 10^8 | 5761455 | 5428681 |
用途很直接:开数组估大小。n = 108 时质数只有 5.76×106 个,primes 数组开 6×106 就够(int 约 23 MB),不用开 108。按 n/ln n 再乘 1.1 估,永远够用。
唯一分解定理的推论:若 n = p1e1p2e2…pkek,则
这说明分解质因数不是一道孤立的题,而是数论题的通用预处理。后面凡是题目里出现“约数”“真因子”“能整除的数有几个”,想到的第一步都该是这个循环。
i <= n / i 版本的试除法判素,并说清为什么不能用 i * i <= n 配 int。if (i % p == 0) break; 的作用,说清删掉它之后哪个结果会错。if (n > 1) 那一句为什么必要。已知正整数 n 恰好是两个不同质数的乘积,求这两个质数中较大的那个。
输入:一行一个正整数 n(6 ≤ n ≤ 2×109),保证 n 是两个不同质数之积。
输出:一行一个正整数,即较大的那个质数。
两个质数 p < q 满足 p·q = n,那么 p 一定不超过 √n。所以从 2 往上试,第一个能整除 n 的数就是 p。
试到什么时候停?想清楚 p 和 √n 的大小关系,循环的上界就出来了。
找到 p 之后不用再找了——q 就是 n/p,它一定是质数,因为 n 只有这两个质因子。
设 n = p·q,p < q 都是质数。由 p < q 得 p·p < p·q = n,所以 p < √n。
从 i = 2 开始往上试,第一个满足 n % i == 0 的 i 就是 p:比 p 小的数都不整除 n(n 只有 p、q 两个质因子);而 p 本身是质数,不会漏。
答案 = n / p。因为 q = n/p,且 q > p。
循环上界用 i <= n / i(等价 i·i ≤ n)。写成 i·i <= n 而 i 是 int 时,n 接近 231 会溢出;写成 i < n 则会 TLE。
复杂度:最坏情况 p 接近 √n ≈ 44721,循环 4 万多轮,远小于 1 秒。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n;
cin >> n;
// n = p * q 且 p < q 都为质数,则 p < √n,从 2 往上找第一个因子即是 p
for (long long i = 2; i <= n / i; ++i) {
if (n % i == 0) { // 第一个因子就是较小的质数 p
cout << n / i << "\n"; // 较大的是 q = n / p
return 0; // 找到即结束,不必再试
}
}
return 0;
}| 片段 | 作用 |
|---|---|
long long n; | n ≤ 2×109,int 装得下,但乘法和上界比较容易出事,统一用 long long 更稳。 |
for (long long i = 2; i <= n / i; ++i) | 上界是 √n。因为 p < q 推出 p < √n,p 一定落在这个范围内;写成 n/i 而不是 i*i,永远不会溢出。 |
if (n % i == 0) | 找到因子。第一个找到的 i 必然是最小的质因子 p——比它小的数都不能整除 n。 |
cout << n / i << "\n"; | 输出较大的那个。注意不是输出 i:题目要的是大的。 |
return 0; | 写完答案立刻退出,避免继续循环。 |
n / i 写成 i:输出的是较小的质数,样例 21 会输出 3 而不是 7。n /= i):后面就要用原来的 n 算 n/i,改了就错。i * i <= n:n 取 231−1 时 i·i 溢出成负数,循环停不下来。i < n:本题因为找到 p 就 return,还能侥幸过关;但换成“判断 n 是否质数”,循环就是 2×109 次,TLE。上界必须写 √n。