算法笔记 / A LEAF IN THE GARDEN

素性筛选

算法笔记 · 算法理论 / 数论。来自 Daily Learning 的学习笔记。

介绍

通过实时判断或预处理,筛选出素数的方法。

暴力试除法

不断除以比自己小的数,若不能被整除则为素数

步骤

  • 从 22 开始除,直到 n\sqrt{n}
  • 若 nn 能被 ii 整除,则 nn 不是素数

复杂度

  • 时间复杂度 O(n)O(\sqrt{n})
  • 空间复杂度 O(1)O(1)

代码

bool is_prime(int n) {
    // 0、1不是素数
    if (n <= 1) return false;
    // 从2试到sqrt(n)
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    // 全都不能整除,是素数
    return true;
}

埃拉托斯特尼筛法

维护一个素数标记列,将素数的所有倍数筛掉

设素数集 P\mathbb{P}

  • p={pi∣pi∈P,pi≤n}p = \{ p_i | p_i \in \mathbb{P}, p_i \leq n\}(所有素数)
  • t={ti∣ti∈N∗,ti≤n}t = \{ t_i | t_i \in \mathbb{N^*}, t_i \leq n\}(所有倍数)

步骤

双层循环

  • 筛选掉所有 p×tp \times t
  • 外层遍历 pp,内层遍历 tt
  • 被外层筛掉的 pp 不会被纳入内层遍历

代码

// 素数集
vector<int> prime; 
// 素数标记列
vector<bool> is_prime;
void filter(int n){
    // 全部设为素数
    is_prime.resize(n, true);
    // 0、1不是素数
    is_prime[0] = is_prime[1] = false;
    // 遍历所有数
    for(int p = 2;p < n;p++){
        // 当前是素数
        if(is_prime[p]){
            // 记录素数
            prime.emplace_back(p);
            // 筛掉倍数
            for(int t = 2;t * p < n;t++)
                is_prime[t * p] = false;
        }
    }
}

欧拉筛法(线性筛)

维护一素数列,将素数列中的每个素数的每个倍数筛掉

设素数集 P\mathbb{P}

  • p={pi∣pi∈P,pi≤n}p = \{p_i | p_i \in \mathbb{P}, p_i \leq n\}(所有素数)
  • t={ti∣ti∈N∗,ti≤n}t = \{t_i | t_i \in \mathbb{N^*}, t_i \leq n\}(所有倍数)

步骤

  • 筛选掉所有 p×tp \times t

问题

有些数被筛选两次

举例

  • p={2,3,5,7,11,...}p = \{2,3,5,7,11,...\}
  • t={1,2,3,4,5,...}t = \{1,2,3,4,5,...\}
    • p1×t=2×{1,2,3,4,5,...}={2,4,6,8,10,...}p_1 \times t = 2 \times \{1,2,3,4,5,...\} \\= \{2,4,6,8,10,...\}
    • p3×t=5×{1,2,3,4,5,...}={5,10,15,20,25,...}p_3 \times t = 5 \times \{1,2,3,4,5,...\} \\= \{5,10,15,20,25,...\}

10出现了两次!

这是因为:

  • p1=t2=2p_1 = t_2 = 2
  • p3=t5=5p_3 = t_5 = 5
  • p1×t5=2×5=10p_1 \times t_5 = 2 \times 5 = 10
  • p3×t2=5×2=10p_3 \times t_2 = 5 \times 2 = 10
  • 当存在 a,b,c,d (a<c,b<d)a,b,c,d\space (a<c,b<d)
  • 满足 pa=tb,pc=tdp_a = t_b, p_c = t_d 时,
  • 会导致 pa×td=pc×tbp_a \times t_d = p_c \times t_b,
  • 该数被筛掉两次

循环逻辑

  • 外层遍历 tt,内层遍历 pp,外层边遍历边添加素数
    • 遍历到 tbt_b 时,pcp_c 还没有被添加到 pp 数列,tb×pct_b \times p_c 不起作用
    • 遍历到 tdt_d 时,pap_a 已经被添加到 pp 数列,td×pat_d \times p_a 起作用
  • 只筛选一次

跳出时机

  • i×pj>ni \times p_j \gt n 时,跳出循环

要筛掉的数超过了搜索范围

  • i % pj=0i\space \%\space p_j = 0 时,跳出循环

pjp_j 是 ii 的因数,

  • 设i=pj×mi = p_j \times m,取 pk (k>j)p_k\space (k\gt j)
  • pk×i=pk×(pj×m)=pj×(pk×m)p_k \times i = p_k \times (p_j \times m) = p_j \times (p_k \times m),
  • 该数已经被 pjp_j 筛掉了,不需要再被 pkp_k 筛选

代码

// 素数集
vector<int> prime; 
// 素数标记列
vector<bool> is_prime;
void filter(int n){
    // 全部设为素数
    is_prime.resize(n, true);
    // 遍历所有倍数t
    for(int t = 2;t < n;t++){
        // 记录素数
        if(is_prime[t]) prime.emplace_back(t);
        // 遍历所有素数p
        for(int p: prime){
            // 越界跳出循环
            if(t * p > n) break;
            is_prime[i * p] = false;
            // 已被筛掉跳出循环
            // 这时候正好p是最小质因子
            if(t % p == 0) break;
        }
    }
}