寻找1-n范围内的质数--埃拉托斯特尼筛法和线性筛法

埃(埃拉托斯特尼)筛

思想

质数的倍数一定不是质数。这个人人都会,超级简单。

步骤

找1-n范围内的质数。

0、首先,2是质数,i = 2

1、如果i是质数(标记为0),令j = i,转到第2步;否则直接转到第3步

2、i * j一定不是质数,将其标记为1。j++。重复此步骤,直到i * j > n

3、i++,如果i * i <= n,转到第2步,否则转到第4步

4、检查所有1-n范围内的数,标记为0的数就是质数。

埃拉托斯特尼筛法的时间复杂度是 O ( n l o g ( l o g n ) ) O(nlog(logn)) O(nlog(logn))。证明需要高深的知识,这里不讲。不过有一点可能有帮助,就是高斯定理:1-n中质数的个数大约为 n / l n   n n/ln\ n n/ln n

int prime[1000000], cnt;
short flag[10000000];

void era(int n) {
    int i, j;
    for (i = 2; i * i <= n; i++) {
        if (flag[i]) continue; // 合数的倍数一定被它的某个质因子筛过,无需重复
        prime[cnt++] = i;
        for (j = i; j * i <= n; j++) flag[i * j]++; // flag数组记录被标记的次数
    }
    for (; i <= n; i++) {
        if (!flag[i]) prime[cnt++] = i;
    }
}

线性筛

思想

我们要想办法把这个 l o g ( l o g n ) log(logn) log(logn)去掉才完美。观察埃拉托斯特尼筛法的过程,可以看到有重复筛的情况,比如,2*6=12,被2的倍数筛一次;3*4=12,又被3的倍数筛一次。这种重复导致埃筛的时间不是严格线性的。可以试着输出flag数组,发现多数合数并不仅被标记一次。可以证明,n的标记的次数就等于它的不同的质因子个数。

显然,每个合数都能分解为质数的乘积的形式(分解质因子),这些质因子中必然有一个最小的。我们线性筛的目的是让每个合数只被筛掉一次,那么一个最简单的方法就是让每个合数仅被最小质因子筛掉。

如何做到呢?我们先分析一下一个合数的分解: t = p ∗ k t=p*k t=pk。如果 p p p是最小质因子,那么首先就可以得出一个大前提: p < t p<t p<t。然后再分两种情况讨论:

1、k是质数,显然 k ≥ p k≥p kp

2、k是合数,将其质因子分解为 k = p 1 r 1 p 2 r 2 . . . p n r n k=p_1^{r_1}p_2^{r_2}...p_n^{r_n} k=p1r1p2r2...pnrn。那么对于任意的 i = 1 , 2 , . . . , n i=1,2,...,n i=1,2,...,n,都有 p i ≥ p p_i≥p pip

上面这个结论非常重要!非常重要!请务必吃透!

线性筛的核心思想是基于 k k k标记 t = p ∗ k t=p*k t=pk;而埃筛是基于 p p p标记 t = p ∗ k t=p*k t=pk。共同点就是都满足 p ≤ k p≤k pk

来看埃筛的核心代码:

for (i = 2; i * i <= n; i++) { // i 就是上面所说的 p
    if (flag[i]) continue;
    prime[cnt++] = i;
    for (j = i; j * i <= n; j++) flag[i * j]++; // j 就是上面所说的 k
}

p是外层循环,k是内层循环。

下面我们改换一下循环策略。埃筛的策略是,外层循环找质数,内层循环找质数的倍数。线性筛的策略是,外层循环i遍历所有的数(无论它是质数还是合数),内层循环j是基于目前已找到的质数的,用来标记i * j,也就是i的质数倍。

由此,可得出线性筛的核心代码:

for (i = 2; i <= n; i++) { // i 就是上面所说的 k
    if (!flag[i]) prime[cnt++] = i;
    for (j = 0; j < cnt && i * prime[j] <= n; j++)
        flag[i * prime[j]]++; // prime[j] 就是上面所说的 p
}

但是别高兴太早!这实现了我们线性筛的目的了吗?并没有,我们只是把埃筛的循环顺序改换了一下而已,实际上,我们标记质数的时候依然有重复,而且重复次数和埃筛一毛一样!(稍微思考一下就懂了)

我们刚才是不是说过最小质因子的概念啊?一定时刻记住,线性筛的核心思想就是每个数都只用最小质因子标记一次。我们忽略了一点,刚才的代码中,prime[j]是否是i * prime[j]的最小质因子。如果不是最小质因子,那就表明i * prime[j]已经被更小的质因子标记过!再次标记就会重复了。下面我们就来解决这个问题。

真正的线性筛

刚才的基本结论说过, t = p ∗ k t=p*k t=pk,如果p是最小质因子,那么k的所有质因子都大于等于p。上面的代码中,对所有i * prime[j]都无脑标记了一遍。但如果i % prime[j] == 0,那这样标记显然无意义了,因为我们标记的是**i的倍数**,此时prime[j]可以整除ii的倍数也一定是prime[j]的倍数,也就是已经被prime[j]标记过了。同时可以得知,prime[j + 1]及后面的其它质数一定不是i * prime[j + 1]的最小质因子,因为i * prime[j + 1]i * prime[j + 2]等等,它们一定能被更小的质因子prime[j]整除。

所以说核心代码很简单,只需要在上面的代码加上一句判断整除:

for (i = 2; i <= n; i++) { // i 就是上面所说的 k
    if (!flag[i]) prime[cnt++] = i;
    for (j = 0; j < cnt && i * prime[j] <= n; j++) {
        flag[i * prime[j]]++; // prime[j] 就是上面所说的 p
        if (i % prime[j] == 0) break; // 后面的质数已经不是最小质因子了
    }
}

这样就一定保证了每个合数只被最小质因子筛一次。所以根据均摊分析,时间复杂度降到了真正的 O ( n ) O(n) O(n)。下面是完整代码:

int prime[1000000], cnt;
short flag[10000000];

void linear(int n) {
    int i, j;
    for (i = 2; i <= n; i++) {
        if (!flag[i]) prime[cnt++] = i;
        for (j = 0; j < cnt && i * prime[j] <= n; j++) {
            flag[i * prime[j]]++;
            if (i % prime[j] == 0) break;
        }
    }
}

再次尝试输出flag数组,发现所有的数标记次数均为0或1。

时间测试

来比较一下两种筛法的性能。为了放大效果,必须用较大的数来测试:

n埃筛运行时间(毫秒)线性筛运行时间(毫秒)
1e6129
5e68554
1e7179123
2e7371246
5e7996564
1e821431173
2e844212278
3e865643432
4e889904651

可以看到线性筛以接近2倍的速度吊打埃筛。

对“线性”的讨论

我们所说的线性筛,其时间复杂度是针对输入的n值来说的。实际上,在时间复杂度理论中,时间复杂度是针对输入规模(直观来说就是输入文件大小)的。整数n的输入规模就是 k = l o g 10 n k=log_{10}n k=log10n,而 O ( n ) O(n) O(n)显然不是k的多项式复杂度,而是指数复杂度。这叫做伪多项式时间(此问题可以称作伪线性时间)。

已经证明,寻找1-n的所有质数是一个NP-hard问题。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值