筛法

一个证明

在讲筛法前,先证明一个东西:

\[对于一个合数 x,一定存在一个质数 p \leq \sqrt{x} 且 p | x。 \]


  • 先把结论考虑简单一点:合数\(x\)一定存在一个小于等于\(\sqrt{x}\)的因数。

    显然一个合数\(x\)可以被一对非\(1\)\(x\)的整数\(p,q\)表示为 \(x=pq\)

    则其中至少有一个整数小于\(x\)

    因为若\(p,q > \sqrt{x}\),那么\(pq > x\)


  • 再进一步考虑这个结论:合数\(x\)一定存在一个小于等于\(\sqrt{x}\)的质因数。

    我们先假设每个合数不存在非\(1\)质因子,

    对于合数\(x\),它一定有一个的小于等于\(\sqrt{x}\)的非\(1\)因数\(p\)

    而已经假设了每个合数不存在质因子,所以\(p\)为合数,

    那么对于合数\(p\), 它一定又有一个小于等于\(\sqrt{p}\)的非\(1\)合数因子\(p_1\),

    \(p_1\)也为\(a\)的因子,

    同上可得:\(p_1\)也一定有一个小于等于\(\sqrt{p_1}\)的非\(1\)合数因子\(p_2\)

    \(p_2\)也为\(a\)的因子,
    ......
    如此循环往复可以做到\(a\)的一个因子\(p_n\leq\sqrt{\sqrt{\sqrt{\sqrt{...}}}}\)
    显然不等号右边的式子可以轻而易举地做到【大于 1 且 小于 2】,

    由于\((1, 2)\)区间内不存在合数,所以假设不成立。

筛法

筛法是对质数和合数进行分类的工具。
假如将所有整数放进一个筛子,那么这个筛子会筛掉合数,留下质数。

一个正确的筛法要同时满足两点:

  1. 留下的都是质数
  2. 筛去的都是合数和\(01\)

本章介绍三种筛法及其优化和正确性证明


普通筛

普通筛是针对某一个单独的整数判断它是否为质数。

先上代码:

bool is_prime(int x) {
    if (x < 2) return false;  
    for (int i = 2; i < x; ++i)  
        if (x % i == 0) 
            return false; 
    return true;  
}

很显然,\(0\)\(1\)都不是质数,这就是第一个判断所做的。
在判断一个数是否是质数时,我们只需要判断它是否有除平凡约数(就是\(1\)和它本身)外的其他约数即可,这就是循环所做的。
这样的时间复杂度是\(O(n)\)

优化

显然对于一个合数\(x\),一定存在一个质数\(p \leq \sqrt{x}\)\(p|x\)
那么我可以将枚举上限设为\(\sqrt{x}\),那样一样可以找到\(x\)的一个因数。
代码如下:

bool is_prime(int x) {
    if (x < 2) return false;  
    for (int i = 2; i * i <= x; ++i)  // 等价于 i <= sqrt(x)
        if (x % i == 0) 
            return false; 
    return true;
}

这样的时间复杂度是\(O(\sqrt{n})\)


埃氏筛

用于确定\([1, n]\)内的质数。

原理很简单,就是质数的倍数即为合数。

那如何实现呢?或者更具体说,如何“先”找到质数?

  1. 先给每个数【都】打上质数标记\(isPrime[i]\),表示数\(i\)是质数。
  2. 外层循环遇到一个质数。
  3. 进入内层循环把【这个质数的倍数】(就是含有该质因数的合数)的“质数标记”删去。
  4. 当外层循环完毕时,剩下【有“质数标记”的】就是质数。

先上代码:

int n;

int prime[maxn], cnt;  // 用来存质数

bitset<maxn> isPrime;  // 质数标记:1 表是质数,0 表不是质数
// 用 bitset 更省空间
// 相当于:bool isPrime[maxn]

void Eratosthenes() {
    isPrime.set();   
    // 相当于:memset(isPrime, true, sizeof(isPrime))
    
    isPrime[0] = isPrime[1] = false;  // 0 和 1 均不为质数
    
    // 实现 1
    for (int i = 2; i <= n; ++i) {   
        if (isPrime[i]) {
            prime[++cnt] = i;
            for (int j = i * 2; j <= n; j += i)
                isPrime[j] = false;
        }
    }
    
    // 实现 2
    // for (int i = 2; i <= n; ++i) {
    //     if (isPrime[i]) prime[++cnt] = i;
    //     for (int j = 1; j <= cnt && i * prime[j] <= n; ++j)
    //         isPrime[i * prime[j]] = false;
    // }
    // 实现 2 主要是为了方便下来讲解在此基础上实现的线性筛
}

正确性

此做法的正确性来源于每个合数都至少有一个质因数(已经证明过了),而每个质数的因子都只有\(1\)和它自己。

想一想当前外层循环运行到\(i=x\)时:
如果它被筛去了,由于循环从\(2\)开始,所以它不可能被\(1\)筛去,那么说明筛去它的是一个大于\(1\)且小于它自己的数,换而言之它存在除平凡因数外的因数,它是个合数;
如果它没被筛去,那么说明它不存在除平凡因数外的因数,它是个质数。
这就证明了埃氏筛的正确性:

  1. 留下的都是质数
  2. 筛去的都是合数和\(01\)

根据证明我们可以明白一件事:\(x\)是质数还是合数在外层循环运行到\(i=x\)前就可以得到判断;当外层循环运行到\(i=x\)时,\([2,x]\)间所有数是质是合都可得到判断。

优化

注:以下优化均以实现1为基础

优化1

先证明一个东西:

\[ [1, i]以内的质数可以把[1,i^2] 内的合数全部筛掉。 \]

很显然当外层循环到\(i\)时,\([1,i]\)内的所有质数和合数均已确定。
现在考虑在\((i, i^2]\)的一个合数\(x\)
根据每个合数至少存在一个质因子,且小于等于\(\sqrt{这个合数}\)
我们可知\(x\)有个质因子小于等于 \(\sqrt{x}\),即在 \([1,i]\)区间内,
由于外层循环已经确定了\([1,i]\)的所有质数,
因此我们可以再通过内层循环将\((i, i * i]\)内的合数筛去。

所以外层循环上限可以改为:

for(int i = 2; i * i <= n; ++i)

延续思路,当\(i\)为质数进入内层循环时,由于\([1,i-1]\)的质数已经把\([1,(i-1)^2]\)的合数筛去了,所以我们\(j\) 要找到一个【大于且离\((i-1)^2\)最近的】\(i\)的倍数作为循环起点,显然起点为\(i(i-1)\),但它又很明显是\(i-1\)的倍数,因此当\(i-1\neq1\)\(i\neq2\)时它一定也被筛去了,所以循环起点定为\(i^2\)

for (int j = i * i; j <= n; j += i)

注:当\(i=2\)时,\(i(i-1)=2\),此时若再将其作为内层循环起点就会把\(isPrime[2]\)标记为\(false\),显然错误。

优化2

显然所有偶数均为合数,所以只需判断一个奇数是否为质数。
结合优化1给出如下代码:

void Eratosthenes() {
    isPrime.set();   
    
    isPrime[0] = isPrime[1] = false;
    for (int i = 4; i <= n; i += 2)
        isPrime[i] = false;  // 严谨一点把的非 2 偶数 isPrime 也删除一下
    
    for (int i = 3; i * i <= n; i += 2) {   
        if (isPrime[i]) {
            for (int j = i * i; j <= n; j += i)
                isPrime[j] = false;
        }
    }
    
    prime[++cnt] = 2;
    for (int i = 3; i <= n; i += 2)
        if (isPrime[i])
            prime[++cnt] = i;
}

时间复杂度为\(O(nln(ln(n)))\)具体证明
注:优化只能省去一些不必要的操作,并不能改变复杂度。


线性筛(欧拉筛)

同样用于确定\([1, n]\)内的质数。

是在埃氏筛实现2的基础上实现的。

先给代码:

void Euler() {
    isPrime.set();
    isPrime[0] = isPrime[1] = false;
    for (int i = 2; i <= n; ++i) {
        if (isPrime[i]) prime[++cnt] = i;
        for (int j = 1; j <= cnt && i * prime[j] <= n; ++j) {
            isPrime[i * prime[j]] = false;
            if (i % prime[j] == 0) break;  // 关键
        }
    }
}

看来线性复杂度的秘诀就在这个关键判断语句上。
下来给出证明:

显然埃氏筛会重复的筛去相同的质数,
如: 考虑\(2\)时,它会筛去\(6,12,18...\)
考虑\(3\)时,它还会筛去\(6,12,18...\)
这就导致了时间复杂度会乘以\(ln(ln(n))\).

那为什么这行判断会省去多余的筛次,
如果\(prime[j]|i\),
\(m=i/prime[j]\),即\(i=x*prime[j]\)
那么\(i*prime[j+k]=x*prime[j]*prime[j+k]\)
\(1 \leq k \leq n-j\)\(prime[j+k]\)其实指的就是\(prime\)数组中剩下的质数),
也就是说\(i*prime[j+k]\)会被\(prime[j]\)筛去,还会被\(prime[j +k]\)筛去,既然它已经被筛了,那就不必麻烦\(prime[j+k]\)再把它筛一遍,所以直接\(break\)

时间复杂度正如它的名字:\(O(n)\)

posted @ 2024-07-27 14:27  syzyc  阅读(45)  评论(0)    收藏  举报