质数筛法

质数筛法

引入

原题链接:P3912 素数个数 - 洛谷

\(1\sim n\) 有多少个质数

朴素求法,时间复杂度 \(O(n\sqrt{n})\)

import java.util.Scanner;

public class Main {
    static boolean isPrime(int x) {
        if (x < 2) return false;
        for (int i = 2, sqrt = (int) Math.sqrt(x); i <= sqrt; ++i)
            if (x % i == 0) return false;
        return true;
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        int cnt = 0;
        for (int i = 1; i <= n; ++i) {
            if (isPrime(i)) ++cnt;
        }
        System.out.println(cnt);
    }
}

时间复杂度太高,考虑其他解法

埃氏筛

对于任意一个大于 \(1\) 的正整数 \(x\),那么他的 \(k\) 倍一定是合数(\(k>1\)),因为该数可以表示成 \(k\times x\)

利用这个结论,如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。

时间复杂度 \(O(n\log\log n)\)

实现

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        boolean[] isPrime = new boolean[n + 1];
        // 初始化2~n均为素数,逐个筛去
        for (int i = 2; i <= n; ++i) isPrime[i] = true;
        for (int i = 2; i <= n; ++i) {
            for (int j = 2 * i; j <= n; j += i) {
                isPrime[j] = false;
            }
        }
        int cnt = 0;
        for (boolean is : isPrime)
            if (is) ++cnt;
        System.out.println(cnt);
    }
}

优化

一、只筛质数的倍数

对于一个合数 \(a\times b\)

如果 \(a\) 是合数,且 \(a\) 是被 \(x\) 筛去的

那么合数 \(a\times b\) 会在筛 \(x\) 的倍数时被筛去

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        boolean[] isPrime = new boolean[n + 1];
        for (int i = 2; i <= n; ++i) isPrime[i] = true;
        for (int i = 2; i <= n; ++i) {
            if (isPrime[i]) {
                for (int j = 2 * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        int cnt = 0;
        for (boolean is : isPrime)
            if (is) ++cnt;
        System.out.println(cnt);
    }
}

二、内循环从 \(i^2\) 筛其倍数

对于一个合数 \(a\times b\),其中 \(a>b>1\)

在用 \(b\) 筛掉这个合数之前,一定会被 \(a\) 先筛去,即一个合数会先被其较小的因子筛去

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        boolean[] isPrime = new boolean[n + 1];
        for (int i = 2; i <= n; ++i) isPrime[i] = true;
        for (int i = 2; i <= n; ++i) {
            // 要注意乘法是否会超出int范围
            if (isPrime[i] && (long) i * i <= n) {
                for (int j = i * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        int cnt = 0;
        for (boolean is : isPrime)
            if (is) ++cnt;
        System.out.println(cnt);
    }
}

三、外循环只筛至 \(\sqrt n\)

上一个优化中提到,一个合数会先被其较小的因子筛去。

而对于 \([\sqrt n,n]\) 内的数均会被小于 \(\sqrt n\) 的因子筛去

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        boolean[] isPrime = new boolean[n + 1];
        for (int i = 2; i <= n; ++i) isPrime[i] = true;
        for (int i = 2, sqrt = (int) Math.sqrt(n); i <= sqrt; ++i) {
            if (isPrime[i]) {
                for (int j = i * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        int cnt = 0;
        for (boolean is : isPrime)
            if (is) ++cnt;
        System.out.println(cnt);
    }
}

四、只筛奇数

我们知道除 \(2\) 以外的偶数均不是合数,因为所有偶数都可以写成 \(2\times x\) 的形式

对此,可以选择只筛奇数

奇数可以表示为 \(2\times x+1\),其中 \(x\in N\)\(x\) 对应质数表的下标

因此,一个数 \(n\) ,判断其是否为质数时,寻找下标为 \(\lfloor\dfrac{n}{2}\rfloor\) 的质数表所对应的值即可。

对于数组长度:

  1. \(n\) 为偶数时,\(1\sim n\) 中最大的奇数为 \(n-1\)
    要用 \(2\times idx+1\) 表示 \(n-1\),即 \(idx_{max}=\dfrac{n-2}{2}\)
    因此需要空间个数为 \(\dfrac{n-2}{2}+1=\dfrac{n}{2}\)

  2. \(n\) 为奇数时,\(1\sim n\) 中最大的奇数为 \(n\)
    要用 \(2\times idx+1\) 表示 \(n\),即 \(idx_{max}=\dfrac{n-1}{2}\)
    因此需要空间个数为 \(\dfrac{n-1}{2}+1=\dfrac{n+1}{2}\)

综上,需要数组空间为 \(\lceil\dfrac{n}{2}\rceil=\lfloor\dfrac{n+1}{2}\rfloor\)

注意此时每次增量应为原来的两倍

import java.util.Scanner;

public class Main {
    static boolean[] primeTable;

    static void initialize(final int n) {
        primeTable = new boolean[(n + 1) / 2];
        for (int i = 1; i < primeTable.length; ++i) primeTable[i] = true;
        for (int i = 3, sqrt = (int) Math.sqrt(n); i <= sqrt; i += 2) {
            if (primeTable[i / 2]) {
                for (int j = i * i; j <= n; j += 2 * i) {
                    primeTable[j / 2] = false;
                }
            }
        }
    }

    static boolean isPrime(int x) {
        if (x % 2 == 0) return x == 2;
        return primeTable[x / 2];
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int n = sc.nextInt();
        initialize(n);
        int cnt = n < 2 ? 0 : 1;
        for (boolean is : primeTable)
            if (is) ++cnt;
        System.out.println(cnt);
    }
}

线性筛

埃氏筛仍有优化思路,因为一些合数可能会被多个因子筛去

算术基本定理

任何一个大于 \(1\) 的自然数 \(N\),如果 \(N\) 不为质数,那么 \(N\) 可以唯一分解成有限个质数的乘积 \(N=P_1^{a_1}P_2^{a_2}P_3^{a_3}\cdots P_n^{an}\),这里 \(P_1<P_2<P_3\cdots<P_n\) 均为质数,其中指数 \(a_i\) 是正整数。这样的分解称为 \(N\) 的标准分解式。

我们选择用每个合数的最小质因子(或者说用这个合数的最大的因子)来筛去这个合数

这样就保证了每个合数只被筛去一次,那么时间复杂度就降低至 \(O(n)\)

实现

将每次遇到的质数存至数组中,若当前遍历到的数记为 \(i\),目前所存的质数为 \(primes_j\),其中\(j>=0\)

则每次只筛取以 \(primes_j\) 为最小质因子, \(i\) 为最大因子的合数

\(i\)\(primes_j\) 的因子时,说明 \(i\) 可以分解\(primes_j\) 这个质数

又因为 \(primes_k\),其中 \(j<k<tot\) ,一定大于 \(primes_j\)

因此,合数 \(i\times primes_k\) 一定会被 \(primes_j\)筛去

举个例子:

\(i=4\) 时,此时 \(primes=2,3\),先标记 \(4\times 2\) 为合数,此时发现 \(2\)\(4\) 的一个因子,则不再继续往后筛,因为 \(4\times 3= 2\times 2\times 3=2\times 6\)

\(i=6\) 时,此时 \(primes=2,3,5\),先标记 \(6\times 2\) 为合数,此时发现 \(2\)\(6\) 的一个因子,则不再继续往后筛,因为 \(6\times 3=2\times 3\times 3=2\times 9\)\(6\times 5=2\times 3\times 5=2\times 15\)

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        int n = new Scanner(System.in).nextInt();
        //除2以外的偶数一定不是质数,只需使用一半空间
        int[] primes = new int[(n + 1) / 2];
        boolean[] isPrime = new boolean[n + 1];
        //tot记录当前质数个数
        int tot = 0;
        for (int i = 2; i <= n; ++i) isPrime[i] = true;
        for (int i = 2; i <= n; ++i) {
            if (isPrime[i]) primes[tot++] = i;
            for (int j = 0; j < tot && i * primes[j] <= n; ++j) {
                isPrime[i * primes[j]] = false;
                if (i % primes[j] == 0) break;
            }
        }
        System.out.println(tot);
    }
}

PS:注意到筛法求素数的同时也得到了每个数的最小质因子,这是筛法求积性函数的铺垫

存储质数数组空间大小

/*
 *@ 作用:欧拉筛1~n的素数
 *@ 返回值:1~n内素数个数
 *@ primes[i]   : 第i+1个素数
 *@ isPrime[i]  : true则i是质数
 */
static int[] primes;
static boolean[] isPrime;
public static int getPrime(final int n) {
    int len = (int) Math.max((n + 1) / (Math.log(n + 1) - 1.112), 1);
    primes = new int[len];
    isPrime = new boolean[n + 1];
    Arrays.fill(isPrime, true);
    isPrime[0] = isPrime[1] = false;
    int tot = 0;
    for (int i = 2, j; i <= n; ++i) {
        if (isPrime[i]) primes[tot++] = i;
        for (j = 0; j < tot && i * primes[j] <= n; ++j) {
            isPrime[i * primes[j]] = false;
            if (i % primes[j] == 0) break;
        }
    }
    return tot;
}

参考资料

筛法 - OI Wiki

posted @ 2023-01-08 00:57  Cattle_Horse  阅读(48)  评论(0编辑  收藏  举报