一、质数
1.因数与质数
a , b a,b a,b 为正整数, b ∣ a b\mid a b∣a 表示 b b b 能整除 a a a,则 b b b 是 a a a 的因数(约数)。
若一个数 a a a,对于所有的 1 ≤ b ≤ a 1\leq b\leq a 1≤b≤a 的整数 b b b,满足 b ∣ a b\mid a b∣a 的只有 b = 1 , b = a b=1,b=a b=1,b=a 两个值,那么 a a a 是一个质数。即数 a a a 只有 1 1 1 和它本身两个因数,则它是一个质数。
2.判断一个数是否为质数
(1)基础版
bool check(ll n)
{
if(n == 1)
return false;
for(ll i = 2; i < n; i++)
if(n % i == 0)
return false;
return true;
}
根据质数的定义,我们依次遍历 2 ∼ n − 1 2\sim n-1 2∼n−1 这 n − 2 n-2 n−2 个数,判断它是否是 n n n 的因数,如果有数 i i i 是 n n n 的因数,那么 n n n 不是一个质数。
此方法的时间复杂度为 O ( n ) O(n) O(n)。
(2)优化版
bool check(ll n)
{
if(n == 1)
return false;
for(ll i = 2; i * i <= n; i++)
if(n % i == 0)
return false;
return true;
}
经过分析我们可以发现一个问题,我们并不需要遍历 2 ∼ n − 1 2\sim n-1 2∼n−1 中所有的数,只需要遍历前 n \sqrt n n 个数即可。因为若 n = a b , a ≤ n n=ab,a\leq\sqrt n n=ab,a≤n,则一定有 b ≥ n b\geq\sqrt n b≥n。即因数是成对出现的,所以我们只需要找出前一个更小的因数即可。
此方法的时间复杂度为 O ( n ) O(\sqrt n) O(n)。
二、筛法求质数
1.最暴力的算法
for(ll i = 2; i <= n; i++)
{
bool flag = false;
for(ll j = 2; j < i; j++)
if(i % j == 0)
flag = true;
if(!flag)
prime[++cnt] = i;
}
直接利用上面讲到的质数定义的判别方式去获取全部的质数,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
2.Eratosthenes 筛法
(1)基础版本
若存在一个数 a a a,那么 2 a , 3 a , 4 a , . . . , n a 2a,3a,4a,...,na 2a,3a,4a,...,na 都不是质数。因为他们至少都包含一个因子 a a a。所以对于每一个数,都可以按照这样的方式筛一遍,若 2 ∼ x − 1 2\sim x-1 2∼x−1 的数都没有筛掉 x x x,那么 x x x 就是一个质数。
for(ll i = 2; i <= n; i++)
is_prime[i] = true;
for(ll i = 2; i <= n; i++)
for(ll j = 2; j * i <= n; j++)
is_prime[j * i] = false;
此筛法的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
(2)优化
for(ll i = 2; i <= n; i++)
is_prime[i] = true;
for(ll i = 2; i * i <= n; i++)
if(is_prime[i])
for(ll j = 2; j * i <= n; j++)
is_prime[j * i] = false;
实际上我们并不需要去遍历 2 ∼ x − 1 2\sim x-1 2∼x−1 中的每一个数,只需要遍历其中的质数即可。例如对于 6 6 6 来说,它的任何倍数都是 2 2 2 的倍数,所以只需要遍历 2 2 2 即可。但是请注意, 6 6 6 的倍数不只会被 2 2 2 筛掉,也会被 3 3 3 筛掉,所以很多的数并不止过筛了一次,这是我们需要去优化的方向。
此筛法的时间复杂度为 O ( n log log n ) O(n\log \log n) O(nloglogn)。
3.线性筛
(1)算法简介
欧拉筛是一个能够做到 O ( n ) O(n) O(n) 的时间复杂度,也就是线性的质数筛法,是目前性能最优秀的质数筛法。在很多算法和数据结构题目中都有大量的应用,是一个十分基础的工具。对于一个频繁使用的工具,我们从原理上掌握它是非常有必要的。
(2)原理分析
- 核心代码如下所示:
for(ll i = 2; i <= n; i++)
{
if(!vis[i])
prime[++count1] = i;
for(ll j = 1; j <= count1 && i * prime[j] <= n; j++)
{
vis[i * prime[j]] = true;
if(i % prime[j] == 0)
break;
}
}
- 算法的特点就是每个数只会被自己的最小质因数筛过一次,所以由此保证了线性的时间复杂度。
- 因为
prime
中素数是递增的,所以如果i % prime[j] != 0
代表 i i i 的最小质因数还没有找到,即i
的最小质因数大于prime[j]
。也就是说prime[j]
就是i * prime[j]
的最小质因数,于是i * prime[j]
被它的最小质因数筛掉了。 - 如果当
i % prime[j] == 0
时,代表i
的最小质因数是prime[j]
,那么i * prime[j + k]
这个合数的最小质因数就不是prime[j + k]
而是prime[j]
了。所以i * prime[j + k]
应该被prime[j]
筛掉,而不是后续的prime[j + k]
。于是在此时break
即可防止一个数被多次遍历,达到线性的效果。 - 综上所述达到了每个数仅筛一次的效果,时间复杂度 O ( n ) O(n) O(n)。
(3)正解代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define Max 1000000
using namespace std;
typedef long long ll;
ll prime[100010], count1 = 0;
bool vis[Max + 10];
void oula()
{
memset(vis, false, sizeof(vis));
for(ll i = 2;i <= Max; i++)
{
if(!vis[i])
prime[++count1] = i;
for(ll j = 1; j <= count1 && i * prime[j] <= Max; j++)
{
vis[i * prime[j]] = true;
if(i % prime[j] == 0)
break;
}
}
}
int main()
{
oula();
for(ll i = 1; i <= 100; i++)
printf("%lld ", prime[i]);
return 0;
}