数论:筛法

一个证明

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

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


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

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

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

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


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

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

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

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

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

    p 1 p_1 p1也为 a a a的因子,

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

    p 2 p_2 p2也为 a a a的因子,

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

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

筛法

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

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

  1. 留下的都是质数
  2. 筛去的都是合数和 01 01 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 0 0 1 1 1都不是质数,这就是第一个判断所做的。
在判断一个数是否是质数时,我们只需要判断它是否有除平凡约数(就是 1 1 1和它本身)外的其他约数即可,这就是循环所做的。
这样的时间复杂度是 O ( n ) O(n) O(n)

优化

显然对于一个合数 x x x,一定存在一个质数 p ≤ x p \leq \sqrt{x} px p ∣ x p|x px
那么我可以将枚举上限设为 x \sqrt{x} x ,那样一样可以找到 x 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 ( n ) O(\sqrt{n}) O(n )


埃氏筛

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

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

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

  1. 先给每个数【都】打上质数标记 i s P r i m e [ i ] isPrime[i] isPrime[i],表示数 i 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 1 1和它自己。

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

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

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

优化

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

优化1

先证明一个东西:

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

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

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

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

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

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

注:当 i = 2 i=2 i=2时, i ( i − 1 ) = 2 i(i-1)=2 i(i1)=2,此时若再将其作为内层循环起点就会把 i s P r i m e [ 2 ] isPrime[2] isPrime[2]标记为 f a l s e false 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 ( n l n ( l n ( n ) ) ) O(nln(ln(n))) O(nln(ln(n)))具体证明
注:优化只能省去一些不必要的操作,并不能改变复杂度。


线性筛(欧拉筛)

同样用于确定 [ 1 , n ] [1, 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 2 2时,它会筛去 6 , 12 , 18... 6,12,18... 61218...
考虑 3 3 3时,它还会筛去 6 , 12 , 18... 6,12,18... 61218...
这就导致了时间复杂度会乘以 l n ( l n ( n ) ) ln(ln(n)) ln(ln(n)).

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值