埃拉托色尼筛法的分段筛法?

41

制作一个简单的筛子非常容易:

for (int i=2; i<=N; i++){
    if (sieve[i]==0){
        cout << i << " is prime" << endl;
        for (int j = i; j<=N; j+=i){
            sieve[j]=1;
        }
    }
    cout << i << " has " << sieve[i] << " distinct prime factors\n";
}

但是如果N非常大,我无法在内存中保存那种类型的数组怎么办?我查阅了分段筛法的方法,它们似乎涉及到找到sqrt(N)以内的质数,但我不明白它是如何工作的。如果N非常大(比如10^18),该怎么办?

我的目标是获取每个k值的唯一质因子数量,对于大N,对于每个k值直到N。


你在回答larsmans时提到,你对于找到大数N的质因子个数非常感兴趣。在这种情况下,假设N < 10^18,分解N比筛选所有小于N的数字要好得多。 - user448810
2
对于每个k,不仅仅是N。 - John Smith
6个回答

56
分段筛法的基本思想是选择小于根号下n的筛素数,选择一个合理的、可以容纳于内存中的大分段大小,然后依次筛选每个分段,从最小的开始。在第一个分段中,计算位于该分段内的每个筛素数的最小倍数,然后按照通常的方式标记筛素数的倍数为合数;当使用完所有的筛素数时,分段中未标记的数字就是质数。然后对于下一个分段,对于每个筛素数,你已经知道当前分段中的第一个倍数(它是该素数在前一个分段中结束筛选的倍数),因此你对每个筛素数进行筛选,以此类推,直到完成。
n的大小并不重要,除了较大的n需要比较长的筛选时间以外;关键的大小是分段的大小,应该尽可能大(例如机器上的主存缓存大小)。
你可以在这里看到分段筛法的简单实现。请注意,相对于 O'Neill 的优先队列筛法,分段筛法会快得多;如果你感兴趣,可以在这里找到一个实现。
编辑:我写这个的初衷不同,但是我想在这里展示一下,因为它可能会有用:
虽然埃拉托斯特尼筛法非常快,但它需要 O(n)的空间。通过在连续的段中执行筛选,可以将其减少到 O(sqrt(n))的筛素数加上 O(1)的位数组。在第一个段中,计算每个筛素数在该段内的最小倍数,然后按照普通方式标记筛选的倍数为合数;当所有筛素数都被使用时,该段剩余未标记的数字即为质数。然后,对于下一个段,每个筛素数的最小倍数是结束上一段筛选的倍数,因此筛选将继续进行,直到完成。
考虑一个筛子的例子,从100到200分段为20个一组。五个筛选质数是3,5,7,11和13。在100到120的第一段中,位数组有十个插槽,插槽0对应于101,插槽k对应于100+2k+1,插槽9对应于119。该段中3的最小倍数是105,对应于插槽2;插槽2+3=5和5+3=8也是3的倍数。5的最小倍数是105,在插槽2上,插槽2+5=7也是5的倍数。7的最小倍数是105,在插槽2上,插槽2+7=9也是7的倍数。以此类推。

函数primesRange接收参数lo、hi和delta;lo和hi必须为偶数,且lo < hi,lo必须大于sqrt(hi)。段大小为两倍的delta。Ps是包含小于sqrt(hi)的筛素的链接列表,在其中2被删除,因为偶数被忽略。Qs是包含相应筛选质数在当前段中最小多个数的偏移量的链接列表。每个段结束后,lo会增加两倍的delta,因此对应于筛选位数组的索引i的数字为lo + 2i + 1。

function primesRange(lo, hi, delta)
    function qInit(p)
        return (-1/2 * (lo + p + 1)) % p
    function qReset(p, q)
        return (q - delta) % p
    sieve := makeArray(0..delta-1)
    ps := tail(primes(sqrt(hi)))
    qs := map(qInit, ps)
    while lo < hi
        for i from 0 to delta-1
            sieve[i] := True
        for p,q in ps,qs
            for i from q to delta step p
                sieve[i] := False
        qs := map(qReset, ps, qs)
        for i,t from 0,lo+1 to delta-1,hi step 1,2
            if sieve[i]
                output t
        lo := lo + 2 * delta

当调用primesRange(100, 200, 10)时,筛法素数ps为[3, 5, 7, 11, 13];qs最初为[2, 2, 2, 10, 8],对应于最小倍数105、105、105、121和117,并在第二段重置为[1, 2, 6, 0, 11],对应于最小倍数123、125、133、121和143。
您可以在http://ideone.com/iHYr1f上看到此程序的运行情况。除了上面显示的链接之外,如果您对使用质数进行编程感兴趣,我谦虚地推荐一下我的博客中的这篇文章

3
你要求提供例子。引用的网站详细展示了如何将100到200范围划分为五个区间进行筛选,包括如何选择筛法质数以及如何为每个区间重置筛法质数。你手工计算过这个例子吗?你还不明白什么? - user448810
6
在观察这个例子时,筛选小于200平方根的质数是3、5、7、11和13。让我们考虑第一个区间,它包括了十个值{101 103 105 107 109 111 113 115 117 119}。在该区间中,3的最小倍数是105,因此去掉105及之后每隔两个数一次的数字:111和117。5的最小倍数是105,因此去掉105及之后每隔四个数一次的数字:115。7的最小倍数是105,因此去掉105及之后每隔六个数一次的数字:119。在该区间中没有11的倍数,因此无需操作。13的最小倍数是 - user448810
5
该段落中的数字117不是素数,因此应该删除。剩下的数字{101、103、107、109、113}都是素数。对于第二个区间{121、123、125、127、129、131、133、135、137、139},每个素数的最小倍数分别为123、125、133(超出区间范围)、121和143(超出区间范围),可以通过计算第一个区间结束后筛选素数的下一个倍数来得到这些数。这能帮到你吗? - user448810
4
非常感谢您对分段筛法和Programming Praxis链接的出色描述,点赞! - NealB
1
它不是第一个大于L且可被P整除的数字,而是筛子内的第一个偏移量。考虑一个长度为P的窗口沿着数轴滑动;在某个时刻,一个端点将大于L,而另一个端点将小于或等于L。从左端点到L的距离是L%P,而-L%P超过L的剩余部分是我们要开始筛选的偏移量。加1并除以2,因为我们的筛子只有奇数,重新排列项,使模P操作在括号外。 - user448810
显示剩余33条评论

7
只是我们使用筛法进行分段处理。 基本思想是,假设我们要在85和100之间找到质数。 我们必须应用传统的筛法,但方式如下所述:
首先取第一个质数2,将起始数字除以2(85/2),并向下取整,得到p=42,现在再乘以2,得到p=84,从这里开始一直加2到最后一个数字。因此,我们已经在该范围内删除了所有2的因子(86、88、90、92、94、96、98、100)。
然后取下一个质数3,将起始数字除以3(85/3),并向下取整,得到p=28,现在再乘以3,得到p=84,从这里开始一直加3到最后一个数字。因此,我们已经在该范围内删除了所有3的因子(87、90、93、96、99)。
依次取下一个质数5等等…… 继续执行上述步骤。您可以通过使用传统的筛法来获得质数(2、3、5、7等),直到sqrt(n),然后对其进行分段筛选。

5

基于优先队列的筛法版本可以按照您的需求生成尽可能多的质数,而不限于在一个上限范围内生成全部质数。经典论文“Eratosthenes真正的筛法”中讨论了此方法,并且在搜索“eratosthenes筛法优先队列”时,会出现各种编程语言的实现。


1
我已经看到了实现,但问题是我不理解它们。那些论文总是非常深奥。我主要在寻找示例,因为我认为这些最容易使用和理解。从技术上讲,我正在使用筛法获取大N下每个值k的唯一质因子数量。 - John Smith
1
Melissa O'Neill在链接的论文中使用的增量筛法与基于数组的筛法相比速度较慢,而且渐进计算复杂度随范围呈现出比线性更快的增长,因此可能不适用于此问题。至于“无需上限”的限定条件,如果当前范围的平方根以下的基础质数被实现为“可扩展数组”或列表形式,则分页分段筛法也不必具有指定的上限。 - GordonBGood
@gordonbgood 对我来说,基于迭代器和优先队列的筛法“比基于数组的筛法慢得多”并不显然正确。就我所知,其运行时间为:O(从i=2到n的log(π(i)-1)ω(i)之和)(其中π是小于或等于给定数的素数数量,ω是给定数的唯一质因数数量)。一个类似天真的基于数组的筛法实现是O(从i=2到n的(n/i)之和,如果i是素数,则为1,如果i不是素数)。 - mtraceur
@gordonbgood 基本上,我没有足够的数学能力来快速思考它,但目前我确实认为你是正确的,前者比后者慢,并且前者的渐近增长比后者更糟糕。但这不是一个非常琐碎的分析,我的初步直觉是怀疑你的评论。我必须像这样明确每次迭代的复杂度,才能看到你似乎是对的(除了主观模糊的加强词语“相当”之外)。 - mtraceur
@mtraceur,最后,关于一系列更易理解的教程答案,请参见我在JavaScript中对分页段SoE的回答以及前面的答案,其中使用数组剪枝来筛选数十亿个数字,每个筛选操作仅需五到六个CPU时钟周期(在Google Chrome浏览器上运行)。该代码片段甚至可以通过将其视为桌面站点在智能手机上运行。由于操作数量减少和每个操作成本降低的两个原因,任何功能筛法都无法与给定语言中的此方法相媲美。 - GordonBGood
显示剩余9条评论

2
如果有人想看C++实现,这是我的代码:
void sito_delta( int delta, std::vector<int> &res)
{

std::unique_ptr<int[]> results(new int[delta+1]);
for(int i = 0; i <= delta; ++i)
    results[i] = 1;

int pierw = sqrt(delta);
for (int j = 2; j <= pierw; ++j)
{
    if(results[j])
    {
        for (int k = 2*j; k <= delta; k+=j)
        {
            results[k]=0;
        }
    }
}

for (int m = 2; m <= delta; ++m)
    if (results[m])
    {
        res.push_back(m);
        std::cout<<","<<m;
    }
};
void sito_segment(int n,std::vector<int> &fiPri)
{
int delta = sqrt(n);

if (delta>10)
{
    sito_segment(delta,fiPri);
   // COmpute using fiPri as primes
   // n=n,prime = fiPri;
      std::vector<int> prime=fiPri;
      int offset = delta;
      int low = offset;
      int high = offset * 2;
      while (low < n)
      {
          if (high >=n ) high = n;
          int mark[offset+1];
          for (int s=0;s<=offset;++s)
              mark[s]=1;

          for(int j = 0; j< prime.size(); ++j)
          {
            int lowMinimum = (low/prime[j]) * prime[j];
            if(lowMinimum < low)
                lowMinimum += prime[j];

            for(int k = lowMinimum; k<=high;k+=prime[j])
                mark[k-low]=0;
          }

          for(int i = low; i <= high; i++)
              if(mark[i-low])
              {
                fiPri.push_back(i);
                std::cout<<","<<i;
              }
          low=low+offset;
          high=high+offset;
      }
}
else
{

std::vector<int> prime;
sito_delta(delta, prime);
//
fiPri = prime;
//
int offset = delta;
int low = offset;
int high = offset * 2;
// Process segments one by one 
while (low < n)
{
    if (high >= n) high = n;
    int  mark[offset+1];
    for (int s = 0; s <= offset; ++s)
        mark[s] = 1;

    for (int j = 0; j < prime.size(); ++j)
    {
        // find the minimum number in [low..high] that is
        // multiple of prime[i] (divisible by prime[j])
        int lowMinimum = (low/prime[j]) * prime[j];
        if(lowMinimum < low)
            lowMinimum += prime[j];

        //Mark multiples of prime[i] in [low..high]
        for (int k = lowMinimum; k <= high; k+=prime[j])
            mark[k-low] = 0;
    }

    for (int i = low; i <= high; i++)
        if(mark[i-low])
        {
            fiPri.push_back(i);
            std::cout<<","<<i;
        }
    low = low + offset;
    high = high + offset;
}
}
};

int main()
{
std::vector<int> fiPri;
sito_segment(1013,fiPri);
}

1

基于Swapnil Kumar的答案,我在C语言中实现了以下算法。它是使用mingw32-make.exe构建的。

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    const int MAX_PRIME_NUMBERS = 5000000;//The number of prime numbers we are looking for
    long long *prime_numbers = malloc(sizeof(long long) * MAX_PRIME_NUMBERS);
    prime_numbers[0] = 2;
    prime_numbers[1] = 3;
    prime_numbers[2] = 5;
    prime_numbers[3] = 7;
    prime_numbers[4] = 11;
    prime_numbers[5] = 13;
    prime_numbers[6] = 17;
    prime_numbers[7] = 19;
    prime_numbers[8] = 23;
    prime_numbers[9] = 29;
    const int BUFFER_POSSIBLE_PRIMES = 29 * 29;//Because the greatest prime number we have is 29 in the 10th position so I started with a block of 841 numbers
    int qt_calculated_primes = 10;//10 because we initialized the array with the ten first primes
    int possible_primes[BUFFER_POSSIBLE_PRIMES];//Will store the booleans to check valid primes
    long long iteration = 0;//Used as multiplier to the range of the buffer possible_primes
    int i;//Simple counter for loops
    while(qt_calculated_primes < MAX_PRIME_NUMBERS)
    {
        for (i = 0; i < BUFFER_POSSIBLE_PRIMES; i++)
            possible_primes[i] = 1;//set the number as prime

        int biggest_possible_prime = sqrt((iteration + 1) * BUFFER_POSSIBLE_PRIMES);

        int k = 0;

        long long prime = prime_numbers[k];//First prime to be used in the check

        while (prime <= biggest_possible_prime)//We don't need to check primes bigger than the square root
        {
            for (i = 0; i < BUFFER_POSSIBLE_PRIMES; i++)
                if ((iteration * BUFFER_POSSIBLE_PRIMES + i) % prime == 0)
                    possible_primes[i] = 0;

            if (++k == qt_calculated_primes)
                break;

            prime = prime_numbers[k];
        }
        for (i = 0; i < BUFFER_POSSIBLE_PRIMES; i++)
            if (possible_primes[i])
            {
                if ((qt_calculated_primes < MAX_PRIME_NUMBERS) && ((iteration * BUFFER_POSSIBLE_PRIMES + i) != 1))
                {
                    prime_numbers[qt_calculated_primes] = iteration * BUFFER_POSSIBLE_PRIMES + i;
                    printf("%d\n", prime_numbers[qt_calculated_primes]);
                    qt_calculated_primes++;
                } else if (!(qt_calculated_primes < MAX_PRIME_NUMBERS))
                    break;
            }

        iteration++;
    }

    return 0;
}

它设置了要查找的质数的最大值,然后使用已知的质数数组进行初始化,例如2、3、5...29。因此,我们制作一个缓冲区,用于存储可能是质数的段,这个缓冲区不能大于这种情况下最大的初始质数29的幂。

我相信有很多可以优化以提高性能的方法,例如并行化分析过程和跳过2、3和5的倍数,但它作为低内存消耗的示例。


0

如果没有任何较小的质数可以整除一个数字,则该数字是质数。由于我们按顺序迭代质数,因此我们已经标记了所有被至少一个质数整除的数字为可整除。因此,如果我们到达一个单元格并且它没有被标记,则它不能被任何更小的质数整除,因此必须是质数。

请记住以下几点:

// Generating all prime number up to  R
 
// creating an array of size (R-L-1) set all elements to be true: prime && false: composite
     

#include<bits/stdc++.h>

using namespace std;

#define MAX 100001

vector<int>* sieve(){
    bool isPrime[MAX];

    for(int i=0;i<MAX;i++){
        isPrime[i]=true;
    }
 for(int i=2;i*i<MAX;i++){
     if(isPrime[i]){
         for(int j=i*i;j<MAX;j+=i){
             isPrime[j]=false;
         }
     }
 }

 vector<int>* primes = new vector<int>();
 primes->push_back(2);
 for(int i=3;i<MAX;i+=2){
     if(isPrime[i]){
     primes->push_back(i);
     }
}

 return primes;
}

void printPrimes(long long l, long long r, vector<int>*&primes){
      bool isprimes[r-l+1];
      for(int i=0;i<=r-l;i++){
          isprimes[i]=true;
      }

      for(int i=0;primes->at(i)*(long long)primes->at(i)<=r;i++){

          int currPrimes=primes->at(i);
          //just smaller or equal value to l
          long long base =(l/(currPrimes))*(currPrimes);
      

          if(base<l){
              base=base+currPrimes;
          }
    
    //mark all multiplies within L to R as false

          for(long long j=base;j<=r;j+=currPrimes){
              isprimes[j-l]=false;
          }

   //there may be a case where base is itself a prime number

          if(base==currPrimes){
              isprimes[base-l]= true;
          }
    }

          for(int i=0;i<=r-l;i++){
              if(isprimes[i]==true){
                  cout<<i+l<<endl;
              }
          

      }
}
int main(){
    vector<int>* primes=sieve();
   
   int t;
   cin>>t;
   while(t--){
       long long l,r;
       cin>>l>>r;
       printPrimes(l,r,primes);
   }

    return 0;

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接