线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数

线性筛法详解:从筛质数到欧拉函数、Möbius 函数与约数函数

一、线性筛解决什么问题

普通的埃氏筛可以在较低复杂度内筛出所有质数,但一个合数可能被多个质数重复筛掉。

线性筛,也叫 Euler 筛,其核心目标是:

让每一个合数只被它的最小质因子筛掉一次。

因此总复杂度可以做到:

O(n) O(n) O(n)

更重要的是,线性筛不仅可以求质数,还可以在筛的过程中同时递推出大量数论函数,例如:

  • 欧拉函数 φ(n)\varphi(n)φ(n);
  • Möbius 函数 μ(n)\mu(n)μ(n);
  • 约数个数 d(n)d(n)d(n);
  • 不同质因子个数 ω(n)\omega(n)ω(n);
  • 最小质因子;
  • 最小质因子的指数;
  • 其他积性函数。

因此真正值得掌握的不是某一份代码,而是:

已知 iii 的信息,如何根据 p∣ip\mid ip∣i 和 p∤ip\nmid ip∤i 两种情况递推出 ipipip 的信息。


二、最基本的线性筛

核心代码如下:

cpp 复制代码
const int N=1e7+100;

int primes[N];
int pcnt;
bool isComposite[N];

void sieve(){
    for(int i=2;i<N;i++){
        if(!isComposite[i]){
            primes[pcnt++]=i;
        }

        for(int j=0;j<pcnt&&1LL*i*primes[j]<N;j++){
            int p=primes[j];

            isComposite[i*p]=true;

            if(i%p==0)
                break;
        }
    }
}

其中:

cpp 复制代码
if(!isComposite[i])

说明 iii 从来没有被更小的质数筛掉,因此 iii 本身是质数。

而最关键的一句是:

cpp 复制代码
if(i%p==0)
    break;

三、为什么一定要 break

设当前枚举:

cpp 复制代码
i

以及质数:

cpp 复制代码
p

并考虑合数:

x=ip x=ip x=ip

线性筛要求 xxx 只由它的最小质因子筛掉。

当第一次出现:

p∣i p\mid i p∣i

时,ppp 已经是 iii 的最小质因子。

如果继续枚举更大的质数 qqq,得到:

iq iq iq

那么 iqiqiq 的最小质因子仍然是 ppp,而不是 qqq。

因此 iqiqiq 应该在之后由 ppp 筛掉,而不应该在这里由 qqq 筛掉。

所以必须:

cpp 复制代码
if(i%p==0)
    break;

正是这句代码保证:

每一个合数只被它的最小质因子处理一次。

因此总复杂度为:

O(n) O(n) O(n)


四、线性筛最重要的分类讨论

在线性筛中,我们始终处理:

x=ip x=ip x=ip

其中 ppp 是质数。

接下来只有两种情况。

情况 1:p∤ip\nmid ip∤i

此时:

gcd⁡(i,p)=1 \gcd(i,p)=1 gcd(i,p)=1

相当于给 iii 加入了一个全新的质因子 ppp。

对于积性函数,通常可以直接利用:

f(ip)=f(i)f(p) f(ip)=f(i)f(p) f(ip)=f(i)f(p)


情况 2:p∣ip\mid ip∣i

此时 ppp 已经是 iii 的质因子。

若:

i=pam,p∤m i=p^am,\quad p\nmid m i=pam,p∤m

那么:

ip=pa+1m ip=p^{a+1}m ip=pa+1m

也就是说:

质因子的种类没有发生变化,只是 ppp 的指数增加了 1。

很多数论函数的递推就是根据这两个情况得到的。


五、线性筛求欧拉函数

欧拉函数定义:

$$

\varphi(n)

#{1\le x\le n:\gcd(x,n)=1}

对于质数 ppp: φ(p)=p−1 \\varphi(p)=p-1 φ(p)=p−1 因此发现质数时: ```cpp phi[i]=i-1; ``` *** ** * ** *** ### 1. 当 p∤ip\\nmid ip∤i 因为: gcd⁡(i,p)=1 \\gcd(i,p)=1 gcd(i,p)=1 利用欧拉函数的积性: ##

\varphi(ip)

\varphi(i)\varphi§

所以: φ(ip)=φ(i)(p−1) \\boxed{ \\varphi(ip)=\\varphi(i)(p-1) } φ(ip)=φ(i)(p−1) 代码: ```cpp phi[i*p]=phi[i]*(p-1); ``` *** ** * ** *** ### 2. 当 p∣ip\\mid ip∣i 设: i=pam i=p\^am i=pam 其中: p∤m p\\nmid m p∤m 根据欧拉函数公式: φ(pa)=pa−1(p−1) \\varphi(p\^a)=p\^{a-1}(p-1) φ(pa)=pa−1(p−1) 所以再乘一个 ppp 后: ##

\varphi(p^{a+1})

p\varphi(p^a)

因此: φ(ip)=pφ(i) \\boxed{ \\varphi(ip)=p\\varphi(i) } φ(ip)=pφ(i) 代码: ```cpp phi[i*p]=phi[i]*p; ``` 所以欧拉函数完整转移为: ```cpp if(i%p==0){ phi[i*p]=phi[i]*p; break; } else{ phi[i*p]=phi[i]*(p-1); } ``` *** ** * ** *** ## 六、线性筛求 Möbius 函数 Möbius 函数定义: μ(n)={1,n=10,n 含平方质因子(−1)r,n 是 r 个不同质数的乘积 \\mu(n)= \\begin{cases} 1,\&n=1\\\\ 0,\&n\\text{ 含平方质因子}\\\\ (-1)\^r,\&n\\text{ 是 }r\\text{ 个不同质数的乘积} \\end{cases} μ(n)=⎩ ⎨ ⎧1,0,(−1)r,n=1n 含平方质因子n 是 r 个不同质数的乘积 对于质数: μ(p)=−1 \\mu(p)=-1 μ(p)=−1 所以: ```cpp mu[i]=-1; ``` *** ** * ** *** ### 1. 当 p∤ip\\nmid ip∤i 此时增加了一个新的质因子,因此不同质因子数量奇偶性改变: μ(ip)=−μ(i) \\boxed{ \\mu(ip)=-\\mu(i) } μ(ip)=−μ(i) *** ** * ** *** ### 2. 当 p∣ip\\mid ip∣i 此时: p2∣ip p\^2\\mid ip p2∣ip 因此 ipipip 含平方质因子: μ(ip)=0 \\boxed{ \\mu(ip)=0 } μ(ip)=0 代码: ```cpp if(i%p==0){ mu[i*p]=0; break; } else{ mu[i*p]=-mu[i]; } ``` *** ** * ** *** ## 七、线性筛求不同质因子个数 定义: ω(n) \\omega(n) ω(n) 表示 nnn 中不同质因子的数量。 例如: 12=22×3 12=2\^2\\times3 12=22×3 所以: ω(12)=2 \\omega(12)=2 ω(12)=2 对于质数: ω(p)=1 \\omega(p)=1 ω(p)=1 *** ** * ** *** 当: p∤i p\\nmid i p∤i 加入了一个新的质因子: ω(ip)=ω(i)+1 \\boxed{ \\omega(ip)=\\omega(i)+1 } ω(ip)=ω(i)+1 当: p∣i p\\mid i p∣i ppp 本来已经存在: ω(ip)=ω(i) \\boxed{ \\omega(ip)=\\omega(i) } ω(ip)=ω(i) 代码: ```cpp if(i%p==0){ omega[i*p]=omega[i]; break; } else{ omega[i*p]=omega[i]+1; } ``` *** ** * ** *** ## 八、线性筛求最小质因子 由于线性筛中一个合数总是由最小质因子筛掉,因此求最小质因子非常自然。 对于质数: ```cpp minPrime[i]=i; ``` 处理: x=ip x=ip x=ip 时,由于 `primes` 从小到大枚举,当前 ppp 就是 xxx 的最小质因子: ```cpp minPrime[x]=p; ``` *** ** * ** *** ## 九、最小质因子的指数 设: n=pam n=p\^am n=pam 其中 ppp 是 nnn 的最小质因子,并且: p∤m p\\nmid m p∤m 我们记录: ```cpp minPrimeExp[n]=a; ``` 对于质数: ```cpp minPrimeExp[p]=1; ``` *** ** * ** *** 如果: p∣i p\\mid i p∣i 说明继续增加同一个最小质因子: minPrimeExp\[ip\]=minPrimeExp\[i\]+1 \\boxed{ minPrimeExp\[ip\]=minPrimeExp\[i\]+1 } minPrimeExp\[ip\]=minPrimeExp\[i\]+1 如果: p∤i p\\nmid i p∤i ppp 是新出现的最小质因子,因此指数为 1: minPrimeExp\[ip\]=1 \\boxed{ minPrimeExp\[ip\]=1 } minPrimeExp\[ip\]=1 *** ** * ** *** ## 十、线性筛求约数个数 设: n=p1a1p2a2⋯prar n=p_1\^{a_1}p_2\^{a_2}\\cdots p_r\^{a_r} n=p1a1p2a2⋯prar 则正约数个数: ##

d(n)

(a_1+1)(a_2+1)\cdots(a_r+1)

对于质数: d(p)=2 d(p)=2 d(p)=2 *** ** * ** *** ### 1. 当 p∤ip\\nmid ip∤i 增加一个指数为 1 的新质因子,因此: d(ip)=2d(i) \\boxed{ d(ip)=2d(i) } d(ip)=2d(i) *** ** * ** *** ### 2. 当 p∣ip\\mid ip∣i 设: i=pam i=p\^am i=pam 其中: p∤m p\\nmid m p∤m 那么: d(i)=(a+1)d(m) d(i)=(a+1)d(m) d(i)=(a+1)d(m) 而: d(ip)=(a+2)d(m) d(ip)=(a+2)d(m) d(ip)=(a+2)d(m) 因此: ##

\boxed{
d(ip)

d(i)\frac{a+2}{a+1}

}

$$

若 minPrimeExp[i]=a,代码就是:

cpp 复制代码
divisorCnt[i*p]
=
divisorCnt[i]
/
(minPrimeExp[i]+1)
*
(minPrimeExp[i]+2);

十一、综合模板

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

const int N=1e7+100;

int primes[N];
int pcnt;

bool isComposite[N];   // 是否为合数
int phi[N];            // 欧拉函数
int mu[N];             // Möbius 函数
int divisorCnt[N];     // 正约数个数
int omega[N];          // 不同质因子个数
int minPrime[N];       // 最小质因子
int minPrimeExp[N];    // 最小质因子的指数

void sieve(){
    phi[1]=1;
    mu[1]=1;
    divisorCnt[1]=1;

    for(int i=2;i<N;i++){
        if(!isComposite[i]){
            primes[pcnt++]=i;

            phi[i]=i-1;
            mu[i]=-1;
            divisorCnt[i]=2;
            omega[i]=1;
            minPrime[i]=i;
            minPrimeExp[i]=1;
        }

        for(int j=0;j<pcnt&&1LL*i*primes[j]<N;j++){
            int p=primes[j];
            int x=i*p;

            isComposite[x]=true;
            minPrime[x]=p;

            if(i%p==0){
                phi[x]=phi[i]*p;
                mu[x]=0;
                omega[x]=omega[i];
                minPrimeExp[x]=minPrimeExp[i]+1;

                divisorCnt[x]
                =
                divisorCnt[i]
                /
                (minPrimeExp[i]+1)
                *
                (minPrimeExp[i]+2);

                break;
            }
            else{
                phi[x]=phi[i]*(p-1);
                mu[x]=-mu[i];
                omega[x]=omega[i]+1;
                minPrimeExp[x]=1;
                divisorCnt[x]=divisorCnt[i]*2;
            }
        }
    }
}

int main(){
    sieve();
    return 0;
}

十二、一个很容易出现的下标错误

如果使用:

cpp 复制代码
primes[++pcnt]=i;

说明质数从:

cpp 复制代码
primes[1]

开始存。

那么枚举时也必须:

cpp 复制代码
for(int j=1;j<=pcnt;j++)

不能从 j=0 开始。

否则:

cpp 复制代码
primes[0]=0

后面可能出现:

cpp 复制代码
i%primes[0]

也就是:

cpp 复制代码
i%0

直接 Runtime Error。

更推荐统一使用 0 下标:

cpp 复制代码
primes[pcnt++]=i;

然后:

cpp 复制代码
for(int j=0;j<pcnt;j++)

这样最不容易出错。


十三、总结

线性筛真正需要掌握的是:

x=ip x=ip x=ip

之后分类:

p∤i p\nmid i p∤i

和:

p∣i p\mid i p∣i

两种情况。

可以简单理解为:

  • p∤ip\nmid ip∤i:加入一个全新的质因子;
  • p∣ip\mid ip∣i:已有质因子的指数增加 1。

因此很多积性函数都可以在线性筛中一起递推。

常见结论:

欧拉函数

φ(ip)={φ(i)(p−1),p∤ipφ(i),p∣i \varphi(ip)= \begin{cases} \varphi(i)(p-1),&p\nmid i\\ p\varphi(i),&p\mid i \end{cases} φ(ip)={φ(i)(p−1),pφ(i),p∤ip∣i

Möbius 函数

μ(ip)={−μ(i),p∤i0,p∣i \mu(ip)= \begin{cases} -\mu(i),&p\nmid i\\ 0,&p\mid i \end{cases} μ(ip)={−μ(i),0,p∤ip∣i

不同质因子个数

ω(ip)={ω(i)+1,p∤iω(i),p∣i \omega(ip)= \begin{cases} \omega(i)+1,&p\nmid i\\ \omega(i),&p\mid i \end{cases} ω(ip)={ω(i)+1,ω(i),p∤ip∣i

约数个数

若 p∤ip\nmid ip∤i:

d(ip)=2d(i) d(ip)=2d(i) d(ip)=2d(i)

若:

i=pam i=p^am i=pam

则:

d(ip)=d(i)a+2a+1 d(ip)= d(i)\frac{a+2}{a+1} d(ip)=d(i)a+1a+2

所以以后看到"要求预处理 1∼n1\sim n1∼n 中大量数论函数值"的题目,可以优先考虑:

这个函数是不是积性函数?能不能在线性筛处理 ipipip 时递推出来?

这比单纯背一份 Euler 筛代码更重要。

相关推荐
好想像大佬一样能够ak所有7 小时前
Leetcode35.搜索插入位置
数据结构·算法·leetcode
aqiu1111117 小时前
【C++算法打怪专栏】LeetCode 165. 比较版本号(双指针与字符串流法)
c++·算法·leetcode
黄金龙PLUS8 小时前
如何基于置换算法来设计对称加密算法
算法·网络安全·密码学·哈希算法·同态加密
不会就选b8 小时前
算法日常・每日刷题--<动态规划>14
算法
赛博守夜人8 小时前
跨境业务与出海合规之九:海外买量投放与营销虚假流量(Ad Fraud):跨国机房群控与归因欺诈识别算法
算法·安全
Joy T8 小时前
Spring AI 接入已有 Java 项目的三种架构设计
java·人工智能·springai·ai入门·chatclient·ai service·ai能力接入
chenbingjie_c8 小时前
C++ STL 适配器:stack & queue,从 vector/list 到 deque 深度解析
数据结构·c++
杨运交8 小时前
[076][核心模块]构建优雅的Java异常处理框架:从错误码到全局异常处理
java·开发语言
caoerzhong9 小时前
仓库数字化成熟度分级:JeeWMS 开源 Java 仓库管理系统如何支撑从可见到自治的四级跃迁
java·开源
MayBaymax9 小时前
RocketMQ 存储机制
java·rocketmq