线性筛的理解和应用

最近在准备相关算法竞赛,正好班里有写博客的活动,就在此记录下自己菜不成声学习的过程。>v<

为算出小于等于n的素数的个数,较自然也是最暴力的的方法便是对每个小于n的正整数进行判定,这样的方法显然达到最优的复杂度,暴力硬解的结果无疑是最终喜提“Time Limit Exceeded”。

为提高算法效率,就要引入“筛”的思想——主要思想是:我们选出一个数n时无论n是素数还是合数,2n,3n,..都是合数,我们无需对这类数进行是否为素数的判断。

Eratosthenes 筛法 (埃拉托斯特尼筛法,简称埃氏筛)

埃氏筛算法的主要思想是:如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。具体算法如下:

int Eratosthenes(int n) {
  int p = 0;
  for (int i = 0; i <= n; ++i) is_prime[i] = 1;
  is_prime[0] = is_prime[1] = 0;
  for (int i = 2; i <= n; ++i) {
    if (is_prime[i]) {
      prime[p++] = i;  // prime[p]是i,后置自增运算代表当前素数数量
      for (int j = i * i; j <= n;
           j += i)  // 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
                    // 的倍数开始,提高了运行速度
        is_prime[j] = 0;  //是i的倍数的均不是素数
    }
  }
  return p;
}

埃氏筛通常可以称为普通筛,结构比较简单,也比较容易理解,其核心就是对找到的素数的倍数通过一次循环进行标记,虽然要额外占用O(n)的内存空间保存标记,但非常高效地减少了程序复杂度。

我们应该注意到,埃氏筛在对数字进行标记筛选时,存在重复筛选,比如6既可以被2筛掉,又可以被3筛掉。原因:任意一个整数可以写成一些素数的乘积

其中p1<p2<p3,这样这个数n能被p1,p2和p3筛掉,反复被标记,尤其在n比较大的时候,n可能有相当多个素因子,被多次标记,显然浪费了时间。

Euler筛法(线性筛)

基于普通筛的不足之处,Euler对其做出了修改——直观地来说,当我们用埃氏筛法对一个素数的n倍进行筛选时,若正在被标记的这个倍数已经足够大,大到超过一特定的数字后,那这个数一定有更大的素因子,能在后续过程中再次被标记,此时就可以停止循环,算法继续对下一个未标记的数进行是否为素数的判断。

通过观察不难发现,若当前正在处理n的i倍数in,i能整除n,那么i与下一个要进行筛选的数的乘积这个合数肯定会被n乘以某个数提前筛掉。因此这里的i便是我们要找的“特定的数字”,利用这一数字提前break掉循环,可以使得每个数字均被筛选一次,将时间复杂度降到最低,这也就是Euler筛的算法思想,实现代码如下所示。

void init() {
  phi[1] = 1;
  for (int i = 2; i < MAXN; ++i) {
    if (!vis[i]) {
      phi[i] = i - 1;
      pri[cnt++] = i;
    }
    for (int j = 0; j < cnt; ++j) {
      if (1ll * i * pri[j] >= MAXN) break;
      vis[i * pri[j]] = 1;
      if (i % pri[j]) {
        phi[i * pri[j]] = phi[i] * (pri[j] - 1);
      } else {
        // i % pri[j] == 0
        // 换言之,i 之前被 pri[j] 筛过了
        // 由于 pri 里面质数是从小到大的,所以 i 乘上其他的质数的结果一定会被
        // pri[j] 的倍数筛掉,就不需要在这里先筛一次,所以这里直接 break
        // 掉就好了
        phi[i * pri[j]] = phi[i] * pri[j];
        break;
      }
    }
  }
}

关键之处在:if(i%prime[j]==0) break;

这句代码保证了每个数最多被筛一次,将时间复杂度降到了线性。

证:prime[]数组中的素数是递增的,当i能整除prime[j],那么iprime[j+1]这个合数肯定会被prime[j]乘以某个数筛掉。因此,这里直接break掉,将iprime[j+1]及之后的给后面的数去筛。这种方法能保证每个数只被筛一遍,又能保证每个数都被筛到。

为了更好的理解,画出前面几次筛的情况:

image-20211029190053110

一般来说,当筛选范围n较小时,埃氏筛和欧氏筛复杂度较相近,甚至埃氏筛表现更好,但随着n的增大,欧氏筛的优越性也逐渐体现出来,可以达到埃氏筛3-4倍的速度。

参考:

1.线性筛的理解及应用 - Rogn - 博客园 (cnblogs.com)

2.线性筛_历尽千帆-CSDN博客_线性筛