← 目录 第 36 / 60 节 全部 60 节

36 · 质数与整除:筛法与分解

把整除问题拆成三个动作:判一个数是否质数、一次筛出一整段的质数、把一个数拆成质因数之积。掌握埃氏筛的两个优化、线性筛的最小质因子、分解循环的 √n 上界,以及 R 很大而区间很小时的区间筛。
普及-预计 60 分钟埃氏筛线性筛质因数分解
本节出现的新函数 / 新语法(先看懂再用)

正整数只有两类:除 1 和自身外没有别的因子的叫质数,有的叫合数。这个划分之所以重要,是因为唯一分解定理:任何大于 1 的整数都能写成若干质数之积,写法唯一。所以数论题的第一步几乎总是“把这个数拆成质数”,而拆之前必须先知道哪些数是质数。

判一个数是不是质数,试到 √n 就够;找一整段的质数,用筛;分解一个数,同样只试到 √n,最后剩下的那部分不用再试。
要做的事数据规模用什么复杂度
判单个 n 是否质数n ≤ 10^12试除到 √n≤ 10^6 次除法
求 2..n 的全部质数n ≤ 10^7埃氏筛O(n log log n)
同上,还要最小质因子n ≤ 10^7线性筛O(n)
分解一个 nn ≤ 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 次除法——六个数量级的差距,就在这一个不等号上。

这一节的两个基础工具都很短,但每一行都有理由。先把结构看清楚,之后所有代码都是它们的变体。

新函数拆解 · 试除法判素
bool isPrime(long long n);
组成含义 / 格式要求
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 次,必须换筛法。

新函数拆解 · 埃氏筛(Sieve of Eratosthenes)
vector<char> isp(n + 1, 1); 然后双重循环把合数标 0
组成含义 / 格式要求
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。

例题 · 质数口袋洛谷 P5723普及-去洛谷做这题 ↗

小 A 有一个“质数口袋”,他从 2 开始依次检查每个自然数,是质数就装进口袋。口袋的负荷是里面所有质数之和,负荷不允许超过 L。

求口袋最多能装下多少个质数,以及此时口袋的负荷。

输入:一个正整数 L(1 ≤ L ≤ 105)。

输出:两行,第一行为最多能装的质数个数,第二行为此时的负荷。

输入: 100 输出: 9 100
例题怎么想:先筛表,再从最小的开始装

题目要“装得尽量多”。所有质数都是正整数,所以装 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;             // ★ 关键的一句,见下表
        }
    }
}
线性筛为什么是 O(n)
代码片段含义与理由
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] 平移成 [0, R−L]
// 统计 [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 时这一行不会误伤。
这一节的 7 种典型错法(含症状)

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(梅滕斯定理)。

nln ln nn log log n 的常数因子
10^52.442.4
10^72.782.8
10^93.033.0
10^123.323.3

这张表的意思是:n 从 105 涨到 1012(一千万倍),常数因子只从 2.4 涨到 3.3。所以埃氏筛几乎就是线性的,重活全在 n 那个乘法里。

素数的个数:π(n) ≈ n / ln n,实际值比估计略大。

nπ(n) 实际值n / ln n
10^595928686
10^67849872382
10^7664579620421
10^857614555428681

用途很直接:开数组估大小。n = 108 时质数只有 5.76×106 个,primes 数组开 6×106 就够(int 约 23 MB),不用开 108。按 n/ln n 再乘 1.1 估,永远够用。

唯一分解定理的推论:若 n = p1e1p2e2…pkek,则

这说明分解质因数不是一道孤立的题,而是数论题的通用预处理。后面凡是题目里出现“约数”“真因子”“能整除的数有几个”,想到的第一步都该是这个循环。

举一反三 · 想清楚这些再往下走
  1. 如果有 105 个不超过 1012 的数要判素,筛法筛不到 1012,逐个试除又要 105×106 次,你会怎么折中?(提示:试除时只除以质数,能省多少?)
  2. 有了 lpf 表以后,怎么把一个数 n 在 O(log n) 内分解完?写出循环,并说明为什么它比试除到 √n 快。
  3. 怎么用筛法一次求出 1..n 每个数的约数个数,而不是逐个分解?状态该怎么设计?
  4. 如果一个合数被它的两个质因子各划了一次,答案会错吗?会错的是哪个量?这解释了线性筛存在的理由吗?
  5. 区间筛的时间花在两段:筛 √R 内的质数、划 [L, R] 的倍数。R = 1012、R−L = 106 时哪段更贵?如果改成 R−L = 102 呢?
  6. 为什么 1 既不算质数也不算合数?如果把 1 当质数,唯一分解定理会怎么破?举个具体的反例。
  7. n = 1018 时还能筛吗?不能的话,判断一个 1018 的数是否质数,有什么别的思路?
自测:能全部回答再进下一节
挑战题 · 质因数分解
洛谷 P1075普及-去洛谷做这题 ↗

已知正整数 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(比如 n /= i):后面就要用原来的 n 算 n/i,改了就错。
  • i 用 int 且条件写 i * i <= n:n 取 231−1 时 i·i 溢出成负数,循环停不下来。
  • 把上界写成 i < n:本题因为找到 p 就 return,还能侥幸过关;但换成“判断 n 是否质数”,循环就是 2×109 次,TLE。上界必须写 √n。
  • 忘了 return:虽然本题能找到因子,但换成“n 本身是质数”的版本就没有输出,养成循环兜底的习惯。
延伸阅读 / 参考