素数筛 -- 埃氏筛 欧拉筛 详解

文章介绍了从基础的暴力枚举素数到更高效的埃氏筛和欧拉筛的过程,通过定理和优化策略提高求解质数问题的效率,特别是欧拉筛法利用最小质因子确保每个合数仅被筛选一次。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、素数筛

1.1 质数

什么是质数?

若a ,b 除±1外无其他公因数,则称a和b互质。

定理1(算术基本定理)

任意正整数n(n != 1)恰有一种方法写成质数的乘积(不计因数乘积的顺序)

定理2(欧几里得)

质数无穷多


1.2 试除法

那么我们初学任意编程语言时,一定会遇到求素数的问题,我们朴素的方法一般都是暴力枚举

 inline bool isprime(int x)
 {
     for (int i = 2; i < x; i++)
         if (x % i == 0)
             return false;
     return true;
 }

1.3 根号优化

从2枚举到 x - 1,如果整除那么就不是素数

代码的正确性毫无疑问 , 但是如果需求量过大,效率就显得低下

这时有人会发现,我们没必要枚举到x - 1,我们只需要枚举到它的平方根即可

(小的那个因数整除,大的必然整除)

如此一来,代码就变成了这个样子

 inline bool isprime(int x)
 {
     int sq = (int)sqrt(x);
     for (int i = 2; i < sq + 1; i++)
         if (x % i == 0)
             return false;
     return true;
 }

优化掉sqrt的二分快速幂的复杂度的话

 inline bool isprime(int x)
 {
     for (int i = 2; i <= x / i; i++)
         if (x % i == 0)
             return false;
     return true;
 }

1.4 埃氏筛

可这样仍然不能满足我们对于效率的要求,显然我们还要另寻出路。

空间换时间是我们算法优化中的一个重要思想,记忆化搜索就是该思想的一种应用

我们每判定出一个素数,对于这个素数乘上大于1的因子的数显然都是合数,如果我们能标记这些合数,我们就能省略去许多

不必要的判断

 const int  maxn = 1e7 + 10;
 int prime[maxn + 1];
 inline bool isprime(int x)
 {
     prime[2] = 0;
     for (int i = 2; i <= maxn / i; i++)
     {
         if (!prime[i])
             for (int j = i * i; j <= maxn; j += i)
                 prime[j] = 1;
     }
     return !prime[x];
 }

这就是我们的埃氏筛

这里解释为什么埃氏筛的j要从i * i而不是2 * i开始, 因为对于大于2的i , 任意 k * i(k < i)它一定在筛k 的 质因数的时候筛过了

这也是我们接下来进一步优化的思想

1.5 欧拉筛

其实这样我们的时间优化已经大大提升了,可是我们发现我们对于每个合数都筛选到x这样一定有重复的情况

比如对于36 , 它可以被4筛 可以被9筛等等,就像kmp算法next数组优化为nextval一样, 我们的埃氏筛也有需要优化的地方

我们先上代码

 const int  maxn = 1e7 + 10;
 int primes[maxn + 1], cnt = 0;//0为素数 1为合数
 int prime[maxn + 1];
 inline bool isprime(int x)
 {
     prime[2] = 0;
     for (int i = 2; i <= x; i++)
     {
         if (!prime[i])
             primes[cnt++] = i;
         for (int j = 0; j < cnt && primes[j] * i <= x; j++)
         {
             prime[primes[j] * i] = 1;
             if (i % primes[j] == 0)
                 break;
         }
     }
     return !prime[x];
 }

这就是素数筛的终极版--欧拉筛

那么为什么欧拉筛能保证每个合数都被筛且只被筛一次呢

还记得我们的定理1(算术基本定理)吗

定理1(算术基本定理)**

任意正整数n(n != 1)恰有一种方法写成质数的乘积(不计因数乘积的顺序)

欧拉筛法核心思想:每个合数只被自己的最小质因子筛一次。

我们prime[i]是从最小的素数开始枚举

i%prime[j] != 0 则说明prime[j]就是我的最小质因数

对 i%prime[j]==0 时即跳出循环的解释:此时可以令 i = k×prime[j],若不跳出,继续筛 i×prime[j+1] 的话,i×prime[j+1] = k×prime[j]×prime[j+1],它的最小质因子显然是 prime[j] 而不是 prime[j+1],违反了筛法原则。

这样就保证了每个合数被最小质因数筛选一次

1.6 筛法求约数个数(补档)

整数n的约数个数计算公式:

很简单,所以不做证明。

逻辑很简单,具体筛法见代码

模板

 std::vector<int> minp, primes;
 std::vector<int> cnt, d;
 ​
 void sieve(int n) {
     minp.assign(n + 1, 0);
     primes.clear();
     cnt.assign(n + 1, 0);
     d.assign(n + 1, 0);
     d[1] = 1;
 ​
     for (int i = 2; i <= n; i++) {
         if (minp[i] == 0) {
             minp[i] = i;
             primes.push_back(i);
             cnt[i] = 1;
             d[i] = 2;
         }
         
         for (auto p : primes) {
             if (i * p > n) {
                 break;
             }
             minp[i * p] = p;
             if (p == minp[i]) {
                 cnt[i * p] = cnt[i] + 1;
                 d[i * p] = d[i] / cnt[i * p] * (cnt[i * p] + 1);
                 break;
             }
             cnt[i * p] = 1;
             d[i * p] = d[i] * 2;
         }
     }
 }

1.7 筛法求约数之和(补档)

整数n的约数和计算公式:

我们筛法过程额外需要两个辅助数组 f , g

f[i] 存放 i 的约数和,g[i] 存放 i 的最小质因子 p 的 幂次和,即 1 + p^1 + p^2 + ...

逻辑很简单,具体筛法见代码

模板

 std::vector<int> minp, primes;
 std::vector<int> f, g;  // f = sum(因子) g = (最小素因子幂和, p^0 + ... p^x)
  
 void sieve(int n) {
     minp.assign(n + 1, 0);
     f.assign(n + 1, 0);
     g.assign(n + 1, 0);
     primes.clear();
     f[1] = 1;
 ​
     for (int i = 2; i <= n; i++) {
         if (minp[i] == 0) {
             minp[i] = i;
             primes.push_back(i);
             f[i] = g[i] = i + 1;
         }
         
         for (auto p : primes) {
             if (i * p > n) {
                 break;
             }
             minp[i * p] = p;
             if (p == minp[i]) {
                 g[i * p] = g[i] * p + 1;
                 f[i * p] = f[i] / g[i] * g[i * p];
                 break;
             }
             g[i * p] = p + 1;
             f[i * p] = f[i] * g[i * p];
         }
     }
 }

1.8 筛法求欧拉函数(补档)

 constexpr int N = 1E7;
 ​
 bool isprime[N + 1];
 int phi[N + 1];
 std::vector<int> primes;
 ​
 std::fill(isprime + 2, isprime + N + 1, true);
 phi[1] = 1;
 for (int i = 2; i <= N; i++) {
     if (isprime[i]) {
         primes.push_back(i);
         phi[i] = i - 1;
     }
     for (auto p : primes) {
         if (i * p > N) {
             break;
         }
         isprime[i * p] = false;
         if (i % p == 0) {
             phi[i * p] = phi[i] * p;
             break;
         }
         phi[i * p] = phi[i] * (p - 1);
     }
 }
 ​


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Equinox

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值