质数筛法

质数筛法

引入

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

1n 有多少个质数

朴素求法,时间复杂度 O(nn)

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×x

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

时间复杂度 O(nloglogn)

实现

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×b

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

那么合数 a×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);
    }
}

二、内循环从 i2 筛其倍数

对于一个合数 a×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);
    }
}

三、外循环只筛至 n

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

而对于 [n,n] 内的数均会被小于 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×x 的形式

对此,可以选择只筛奇数

奇数可以表示为 2×x+1,其中 xNx 对应质数表的下标

因此,一个数 n ,判断其是否为质数时,寻找下标为 n2 的质数表所对应的值即可。

对于数组长度:

  1. n 为偶数时,1n 中最大的奇数为 n1
    要用 2×idx+1 表示 n1,即 idxmax=n22
    因此需要空间个数为 n22+1=n2

  2. n 为奇数时,1n 中最大的奇数为 n
    要用 2×idx+1 表示 n,即 idxmax=n12
    因此需要空间个数为 n12+1=n+12

综上,需要数组空间为 n2=n+12

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

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=P1a1P2a2P3a3Pnan,这里 P1<P2<P3<Pn 均为质数,其中指数 ai 是正整数。这样的分解称为 N 的标准分解式。

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

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

实现

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

则每次只筛取以 primesj 为最小质因子, i 为最大因子的合数

iprimesj 的因子时,说明 i 可以分解primesj 这个质数

又因为 primesk,其中 j<k<tot ,一定大于 primesj

因此,合数 i×primesk 一定会被 primesj筛去

举个例子:

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

i=6 时,此时 primes=2,3,5,先标记 6×2 为合数,此时发现 26 的一个因子,则不再继续往后筛,因为 6×3=2×3×3=2×96×5=2×3×5=2×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 @   Cattle_Horse  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示