【算法专题】筛法求质数

筛法求质数

1. 概述

  • 判断一个数n是否是质数,可以使用试除法,时间复杂度是 O ( n ) O(\sqrt n) O(n )的。

  • 现在的问题是求1~n中的所有质数,如果一个一个判断的话,时间复杂度是 O ( n × n ) O(n \times \sqrt n) O(n×n )的,不可取。

筛质数

  • 所谓的筛质数是指:给定一个正整数n,输出1~n中的质数。

  • 存在三种筛质数的方法:(1)朴素法筛质数;(2)埃拉托色尼筛选法;(3)线性选法。

  • 这三种方法对数据的存储是相同的,如下:

const int N = 1000010;

int primes[N], cnt;  // primes[0]~primes[cnt-1]存储的是0~n中所有的质数(从小到大)
bool st[N];  // st[i] == true说明i不是质数

(1)朴素法筛质数

  • 基本思路:将2~n中所有数的倍数都删掉,剩余的数据就是质数。

  • 从2一直遍历到n,如果当前st[i]==false,说明i是质数,因为它没被2~i-1筛掉,说明2~i-1都不能整除i,根据定义i是质数。

  • 时间复杂度分析:我们要删除2的所有倍数,3的所有倍数,…,因此计算次数为 n 2 + n 3 . . . + n n = n × ( 1 2 + 1 3 . . . + 1 n ) \frac{n}{2} + \frac{n}{3} ... + \frac{n}{n} =n \times (\frac{1}{2} + \frac{1}{3} ... + \frac{1}{n}) 2n+3n...+nn=n×(21+31...+n1),其中 1 2 + 1 3 . . . + 1 n \frac{1}{2} + \frac{1}{3} ... + \frac{1}{n} 21+31...+n1是调和级数,该级数是发散的,但有: 1 2 + 1 3 . . . + 1 n = l n ( n ) \frac{1}{2} + \frac{1}{3} ... + \frac{1}{n}=ln(n) 21+31...+n1=ln(n),因此时间复杂度为 O ( n × l o g ( n ) ) O(n\times log(n)) O(n×log(n))

// 朴素法筛质数,时间复杂度:O(n*log(n))
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = i + i; j <= n; j += i) st[j] = true;
    }
}

(2)埃拉托色尼筛选法(埃式筛法)

  • 该方法是对(1)的一个优化。

  • 基本思路:将2~n中所有质数的倍数都删掉,剩余的数据就是质数。

  • 从2一直遍历到n,如果当前st[i]==false,说明i是质数,因为它没被2~i-1中的质数筛掉,说明2~i-1中的质数都不能整除i,则i是质数。

  • 时间复杂度分析:这里大致估计一下,首先由质数定理:1~n中质数的个数大约为 n l n ( n ) \frac{n}{ln(n)} ln(n)n个,本来要筛n个数,但是现在只需要筛大约为 n l n ( n ) \frac{n}{ln(n)} ln(n)n个数,因此时间复杂度为 n / l o g ( n ) × l o g ( n ) = n n / log(n) \times log(n)=n n/log(n)×log(n)=n,为 O ( n ) O(n) O(n)量级的。

  • 实际上,埃式筛法准确的时间复杂度为 O ( n × l o g ( l o g ( n ) ) ) O(n \times log(log(n))) O(n×log(log(n))),这是因为我们的计算次数是 n 2 + n 3 + n 5 + . . . = n × ( 1 2 + 1 3 + 1 5 . . . ) = n × l o g ( l o g ( n ) ) \frac{n}{2} + \frac{n}{3} + \frac{n}{5} + ... =n \times (\frac{1}{2} + \frac{1}{3} + \frac{1}{5} ... ) = n \times log(log(n)) 2n+3n+5n+...=n×(21+31+51...)=n×log(log(n))

// 埃拉托色尼筛选法,时间复杂度:O(n*log(log(n)))
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) {
            primes[cnt++] = i;
            for (int j = i + i; j <= n; j += i) st[j] = true;
        }
    }
}

(3)线性选法

  • 这和前两种方法没有什么关系,其是另一种思路。

  • 基本思路:每个数只用它最小的质因子筛掉。

  • 时间复杂度分析:因为每个数只会被它最小的质因子筛掉,因此每个数只会被遍历一次,时间复杂度是 O ( n ) O(n) O(n)的,当 n = 1 0 6 n=10^6 n=106时,线性筛法和埃式筛法效率差不多,当 n = 1 0 7 n=10^7 n=107时,线性筛法速度是埃式筛法的两倍。

// 线性选法,时间复杂度:O(n)
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
  • 分析如下:
// 线性选法,时间复杂度:O(n)
void get_primes(int n) {
    for (int i = 2; i <= n; i++) {
        if (!st[i]) prime[cnt++] = i;
        // 这里没必要写 j < cnt, 因为i如果为合数的话,枚举到i的最小质因子后一定会停下来
        // i如果为质数的话,枚举到primes[cnt - 1]=i时一定会停下来
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
/**
 * primes[j]记为pj,因为从小到大枚举质因子,所以:
 * (1) i % pj == 0 说明:pj是i的最小质因子,pj也是 pj*i的最小质因子
 * (2) i % pj != 0 说明:pj一定小于i的所有质因子,pj也是 pj*i的最小质因子
 * 另外:对于任意一个合数x,都会被筛掉:假设pj是x的最小质因子,当i枚举到
 *      x/pj 的时候,pj已经被枚举过(pj <= x/pj),x会pj被筛掉
 * --> 因为每个合数都会被筛掉,而且每个数只会被它的最小质因子筛掉,因此每个数
 *     只会被筛一次,时间复杂度是O(n)的
 */

2. 例题

AcWing 868. 筛质数

问题描述

  • 问题链接:AcWing 868. 筛质数

    【算法专题】筛法求质数_第1张图片

分析

  • 概述中筛质数的三种方法随便选一种即可。

代码

  • C++
#include 

const int N = 1000010;

int n;
int primes[N], cnt;
bool st[N];  // st[x]存储x是否被筛掉,为true代表是合数

/*********************************/
// // 朴素法筛质数: O(n * log(n))
// void get_primes() {
    
//     for (int i = 2; i <= n; i++) {
//         if (!st[i]) primes[cnt++] = i;
//         for (int j = i + i; j <= n; j += i) st[j] = true;
//     }
// }

/*********************************/
// // 埃拉托色尼筛法: O(n * log(log(n))
// void get_primes() {
    
//     for (int i = 2; i <= n; i++) {
//         if (!st[i]) {
//             primes[cnt++] = i;
//             for (int j = i + i; j <= n; j += i) st[j] = true;
//         }
//     }
// }

/*********************************/
// 线性筛法: O(n)
void get_primes() {
    
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = 0; primes[j] * i <= n; j++) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    
    scanf("%d", &n);
    
    get_primes();
    
    printf("%d\n", cnt);
    
    return 0;
}

AcWing 196. 质数距离

问题描述

  • 问题链接:AcWing 196. 质数距离

    【算法专题】筛法求质数_第2张图片

分析

  • 本题中要求求解在区间[L, R]之间相邻质数距离的最小值和最大值,我们筛出[L, R]之间的质数,然后遍历一遍即可得到答案。

  • 因为线性筛法只能从1开始筛,因此一种基本做法是将1~R中的所有质数筛出来,然后在区间[L, R]中扫描一遍即可。但是这种做法对于本题不可行,因为L、R最大为 2 31 − 1 = 2147483647 2^{31} - 1 = 2147483647 2311=2147483647,二十多亿的计算量,空间也会超,不可行。

  • 仔细读题可以发现 R − L ≤ 1 0 6 R - L \le 10 ^ 6 RL106,我们可以考虑从这一点出发,考入如何只筛区间[L, R],对于一个合数x,则其一定存在一个质因子p,且 p ≤ n p \le \sqrt n pn ,因此我们只需要将 1 1 1~ 2 31 − 1 \sqrt {2 ^ {31} - 1} 2311 之间的质数筛出来,大于需要筛除1~50000之间的质数即可。

  • 则对于[L, R]之间的任意一个合数x,则其必定存在一个质因子p,其值在1~50000之间且 p < x p < x p<x;反之也成立。即x是合数    ⟺    \iff x存在一个小于自己的质因子。

  • 因此,本题的步骤是:

    (1)找出1~50000中所有的质因子;

    (2)对于1~50000中的每个质数p,将[L, R]中的所有p的倍数筛掉(至少是两倍);

    (3)遍历[L, R]之间的质数,找出距离最小值和最大值。

  • 现在考虑第(2)步如何处理:如何枚举[L, R]中所有p的倍数呢?我们只需要找到大于等于L的最小的p的倍数即可,该数值为 ⌈ L p ⌉ × p = ⌊ L + p − 1 p ⌋ × p \lceil \frac{L}{p} \rceil \times p = \lfloor \frac{L + p - 1}{p} \rfloor \times p pL×p=pL+p1×p。另外至少要是两倍,因此需要从 m a x ( p × 2 , ⌊ L + p − 1 p ⌋ × p ) max(p \times 2, \lfloor \frac{L + p - 1}{p} \rfloor \times p) max(p×2,pL+p1×p)开始枚举。

  • 时间复杂度:假设区间长度为 1 0 6 10^6 106,对于每个质数p,最多有 1 0 6 p \frac{10^6}{p} p106p的倍数,因此

1 0 6 2 + 1 0 6 3 + 1 0 6 5 + . . . = 1 0 6 × ( 1 2 + 1 3 + 1 5 + . . . ) = 1 0 6 × l o g ( l o g ( 1 0 6 ) ) \frac{10^6}{2} + \frac{10^6}{3} + \frac{10^6}{5} + ... = 10 ^ 6 \times (\frac{1}{2} + \frac{1}{3} + \frac{1}{5} + ...) = 10^6 \times log(log(\sqrt{10^6})) 2106+3106+5106+...=106×(21+31+51+...)=106×log(log(106 ))

即时间复杂度为 O ( n × l o g ( l o g ( n ) ) ) O(n \times log(log(\sqrt{n}))) O(n×log(log(n )))的。

代码

  • C++
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 1000010;

int primes[N], cnt;  // 刚开始存储[1~50000]之间的质数, 然后被复用, 存储[L, R]之间的质数
int st[N];  // 也会被复用, 表示是否被筛掉

void init(int n) {
    
    memset(st, 0, sizeof st);
    cnt = 0;
    
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = 0; primes[j] * i <= n; j++) {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int main() {
    
    int l, r;
    while (cin >> l >> r) {
        
        // (1) 找出1~50000中所有的质因子
        init(50000);
        
        // (2) 对于1~50000中的每个质数p,将[L, R]中的所有p的倍数筛掉(至少是两倍)
        memset(st, 0, sizeof st);
        for (int i = 0; i < cnt; i++) {
            LL p = primes[i];
            for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p)
                st[j - l] = true;  // 代表区间[L, R]中的数据j是合数
        }
        
        cnt = 0;
        for (int i = 0; i <= r - l; i++)
            if (!st[i] && i + l >= 2)  // i+l = 1时,1不是质数
                primes[cnt++] = i + l;
        
        if (cnt < 2) puts("There are no adjacent primes.");
        else {
            int minp = 0, maxp = 0;
            for (int i = 0 ; i + 1 < cnt; i++) {
                int d = primes[i + 1] - primes[i];
                if (d < primes[minp + 1] - primes[minp]) minp = i;
                if (d > primes[maxp + 1] - primes[maxp]) maxp = i;
            }
            
            printf("%d,%d are closest, %d,%d are most distant.\n",
                primes[minp], primes[minp + 1],
                primes[maxp], primes[maxp + 1]);
        }
    }
    
    return 0;
}

AcWing 197. 阶乘分解

问题描述

  • 问题链接:AcWing 197. 阶乘分解

    【算法专题】筛法求质数_第3张图片

分析

  • 阶乘的质因数分解: n ! n! n!所有的质因子一定是小于等于n的,否则如果存在大于n的质因子,说明该质因子一定是某几个数相乘得到的,违反质数的定义。

    (1)求出1~n中所有的质数,可以使用线性法筛质数;

    (2)枚举某个质数p,则其在n!的质因数分解中出现的次数为:
    ⌊ n p ⌋ + ⌊ n p 2 ⌋ + . . . . . . \lfloor \frac{n}{p} \rfloor + \lfloor \frac{n}{p^2} \rfloor + ...... pn+p2n+......
    该方法求阶乘 n ! n! n!的质因数分解的时间复杂度大约为 O ( n ) O(n) O(n),因为1~n中大约有 n l o g ( n ) \frac{n}{log(n)} log(n)n个质数,求每个质数出现次数计算大约为 l o g ( n ) log(n) log(n)的,相乘得到阶乘质因数分解时间复杂度大约为 O ( n ) O(n) O(n)的。

  • 另外指的一提的是:AcWing 888. 求组合数 IV这一题中就用到了阶乘的质因数分解。

代码

  • C++
#include 

using namespace std;

const int N = 1000010;

int primes[N], cnt;
bool st[N];

void get_primes(int n) {
    
    for (int i = 2; i <= n; i++) {
        if (!st[i]) primes[cnt++] = i;
        for (int j = 0; primes[j] <= n / i; j++) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    
    int n;
    scanf("%d", &n);
    
    get_primes(n);
    
    for (int i = 0; i < cnt; i++) {
        int p = primes[i];
        
        int s = 0, t = n;
        while (t) s += t / p, t /= p;
        
        printf("%d %d\n", p, s);
    }
    
    return 0;
}

你可能感兴趣的:(算法专题,线性筛法,埃拉托斯特尼筛法)