给定所有先前的质数,找到下一个质数

6
我正在编写一个递归无限质数生成器,我几乎可以肯定我可以更好地优化它。
现在,除了前十二个质数的查找表外,每次调用递归函数都会接收之前所有质数的列表。
由于它是懒惰的生成器,现在我只是过滤掉任何模数为之前任何质数的结果,并取第一个未过滤的结果。(我使用的检查可以短路,所以第一次之前的质数将当前数字均匀地分割时,它会中止并返回信息。)
目前,我的性能在搜索第400个质数(37,813)时下降。我正在寻找使用我拥有所有先前质数的唯一事实,并仅搜索下一个来改进我的过滤算法的方法。(我可以找到的大多数信息提供非懒惰筛来查找极限以下的质数,或者通过给出pn-1的方式找到第pn个质数,而不是找到pn给出2...pn-1个质数的优化。)
例如,我知道第pn个质数必须驻留在(pn-1 + 1)...(pn-1+pn-2)范围内。现在我从pn-1 + 2开始对整数进行过滤(因为pn-1 + 1只能为质数,如果pn-1=2,则为预先计算的)。但由于这是一个懒生成器,知道范围(pn-1+pn-2)的终端边界并不能帮助我过滤任何东西。
在拥有所有先前质数的情况下,我可以做些什么来更有效地过滤?
  @doc """
  Creates an infinite stream of prime numbers.

    iex> Enum.take(primes, 5)
    [2, 3, 5, 7, 11]

    iex> Enum.take_while(primes, fn(n) -> n < 25 end)
    [2, 3, 5, 7, 11, 13, 17, 19, 23]

  """
  @spec primes :: Stream.t
  def primes do
    Stream.unfold( [], fn primes ->
      next = next_prime(primes)
      { next, [next | primes] }
    end )
  end

  defp next_prime([]),      do: 2
  defp next_prime([2 | _]), do: 3
  defp next_prime([3 | _]), do: 5
  defp next_prime([5 | _]), do: 7
  # ... etc

  defp next_prime(primes) do
    start = Enum.first(primes) + 2
    Enum.first(
      Stream.drop_while(
        Integer.stream(from: start, step: 2),
        fn number ->
          Enum.any?(primes, fn prime ->
            rem(number, prime) == 0
          end )
        end
      )
    )
  end
primes函数从一个空数组开始,获取下一个质数(最初为2),然后1)从流中发出它并2)将其添加到下一次调用中使用的质数堆栈的顶部。(我确定这个堆栈是某些减速的源头。) next_primes函数接受该堆栈。从上一个已知质数+2开始,它创建一个无限的整数流,并丢弃每个可以被列表中任何已知质数均匀地整除的整数,然后返回第一个出现的值。
这类似于一种惰性增量埃拉托斯特尼筛法。
您可以看到一些基本的优化尝试:我从pn-1+2开始检查,并跳过偶数。
我尝试了更直接的埃拉托斯特尼筛法,只需通过每个计算传递Integer.stream,并在找到质数后,在新的Stream.drop_while中包装Integer.stream,仅过滤掉那些质数的倍数。但由于Streams是作为匿名函数实现的,所以这破坏了调用栈。
值得注意的是,我并不假设您需要所有先前的质数来生成下一个质数。我只是碰巧拥有它们,感谢我的实现。

你的问题不够清晰。你想一直生成质数吗?如果你发布一些代码,可能会更加明确。 - Abhishek Bansal
1
“bogs down”是什么意思?我刚刚用C#写了一个小的质数生成器,可以在大约1.8毫秒内生成前2500个质数。总共。它可以在大约150毫秒内生成所有小于一百万的质数。而且这段代码并没有高度优化。我怀疑你的实现不太对。给我们看看你的代码。否则我们真的无法帮助你。 - Jim Mischel
1
@JimMischel 55,555 !!!!!! :) - Will Ness
@WillNess:请看我的简单C#代码。在我的系统中,发布模式下它可以在0.8毫秒内完成前2500个质数的计算,而前78,500个质数则需要83毫秒。在ideone上的计时结果...要慢得多。正如你所看到的,我使用了一种不同的计算质数的方法。我将质数添加到动态列表中,而不是使用从0到N的数组。这就解释了我们计时结果中大部分的差异。 - Jim Mischel
@JimMischel,你的代码是试除法。[它的运行时间为~n^1.43](http://ideone.com/weY3dxhttp://ideone.com/weY3dx),生成了`n`个质数。SoE应该会快得多。 :) - Will Ness
显示剩余7条评论
3个回答

7
对于任何数字k,您只需要尝试使用不超过√k的素数进行除法。这是因为任何大于√k的素数都需要与小于√k的素数相乘。
证明: √k * √k = k,所以(a+√k) * √k > k(对于所有0 < a < (k-√k))。由此可知,(a+√k)能够整除k,当且仅当有另一个小于√k的因子。
这通常用于极大地加快查找质数的速度。

2
不是O(log n)。使用此方法生成n个质数的时间复杂度为O(n^1.5/(log n)^0.5)。使用埃拉托斯特尼筛法生成n个质数的时间复杂度为O(n log n log log n)。检查k是否为质数的时间复杂度为pi(sqrt(k))2sqrt(k)/log(k),其中k=n log n(n是小于k的质数数量)。即2sqrt(n/log n)。 - Will Ness
Will Ness,你当然是正确的!我完全忘记了k和n之间存在非线性相关性。我甚至不确定否则我是否正确,但重点仍然是它具有完全不同的渐近运行时间。谢谢! - Emil Vikström

5
你不需要所有之前的质数,只需要当前产生点下面的平方根以下的那些质数就足够了,在使用埃拉托斯特尼筛法生成合数时。
这大大降低了内存需求。然后质数只是那些不在合数中的奇数。
每个质数p产生一个从它的平方开始的倍数链,以2p的步长枚举(因为我们只处理奇数)。这些倍数和它们的步长值被存储在字典中,从而形成一个优先队列。只有小于当前候选数的平方根的质数存在于这个优先队列中(与分段筛E.相同的内存要求)。
符号上,埃拉托斯特尼筛法是
P = {3,5,7,9, ...} \ ⋃ {{p^2, p^2+2p, p^2+4p, p^2+6p,...} | p in P}
每个奇素数通过重复加法生成其倍数流; 所有这些流合并在一起给出所有的奇合数; 而质数就是没有合成数(以及一个偶素数2)的所有奇数。 在Python中(可以视为可执行的伪代码,希望如此)。
def postponed_sieve():      # postponed sieve, by Will Ness,   
    yield 2; yield 3;       #   https://dev59.com/K3E95IYBdhLWcg3wp_yn#10733621
    yield 5; yield 7;       # original code David Eppstein / Alex Martelli 
    D = {}                  #   2002, http://code.activestate.com/recipes/117119
    ps = (p for p in postponed_sieve())  # a separate Primes Supply:
    p = ps.next() and ps.next()          #   (3) a Prime to add to dict
    q = p*p                              #   (9) when its sQuare is 
    c = 9                                #   the next Candidate
    while True:
        if c not in D:                # not a multiple of any prime seen so far:
            if c < q: yield c         #   a prime, or
            else:   # (c==q):         #   the next prime's square:
                add(D,c + 2*p,2*p)    #     (9+6,6 : 15,21,27,33,...)
                p=ps.next()           #     (5)
                q=p*p                 #     (25)
        else:                         # 'c' is a composite:
            s = D.pop(c)              #   step of increment
            add(D,c + s,s)            #   next multiple, same step
        c += 2                        # next odd candidate

def add(D,x,s):                          # make no multiple keys in Dict
    while x in D: x += s                 # increment by the given step
    D[x] = s  

一旦产生了质数,就可以将其遗忘。为了维护字典,从同一生成器的另一个递归调用中获取单独的质数供应。而该质数供应也是递归地从另一个质数供应中获取的。每个供应只需要提供到其生产点的平方根即可,因此总体上只需要很少的生成器(数量级为log log N),它们的大小渐近无穷小(sqrt(N)sqrt( sqrt(N) )等)。

这很精妙。但是对于一个无限懒惰生成器,难道不是必须保留所有之前的质数吗?我不太确定您所说的通过忘记产生的质数可以节省内存。 - Emil Vikström
不是所有的质数,只有小于当前候选数的那些。它们不仅仅被视为质数,而是作为字典条目存储,形式为(multiple,2 * prime),以便在需要时生成该质数的下一个倍数,如next_mult = mult + 2 * p。我们可以忘记所产生的质数,即只需将其打印到某个文件中或添加到总和变量中等。 - Will Ness
是的,正确;因此,需要向字典询问下一个倍数,即下一个合数。如果它大于候选数,则该候选数是质数。对于候选数k,字典的大小将为O(pi(sqrt(k))),即~2sqrt(k)/log(k)。 - Will Ness
1
在这里发布的许多引人入胜的答案、优化和算法中,这个似乎最符合我的方法。谢谢! - Chris Keele

4

我写了一个程序,按顺序生成质数,没有限制,并用它来计算我的博客中的前十亿个质数之和。该算法使用分段埃拉托色尼筛法;在每个段中计算额外的筛选质数,因此只要有空间存储筛选质数,该过程可以无限继续下去。以下是伪代码:

function init(delta) # Sieve of Eratosthenes
  m, ps, qs := 0, [], []
  sieve := makeArray(2 * delta, True)
  for p from 2 to delta
    if sieve[p]
      m := m + 1; ps.insert(p)
      qs.insert(p + (p-1) / 2)
      for i from p+p to n step p
        sieve[i] := False
  return m, ps, qs, sieve

function advance(m, ps, qs, sieve, bottom, delta)
  for i from 0 to delta - 1
    sieve[i] := True
  for i from 0 to m - 1
    qs[i] := (qs[i] - delta) % ps[i]
  p := ps[0] + 2
  while p * p <= bottom + 2 * delta
    if isPrime(p) # trial division
      m := m + 1; ps.insert(p)
      qs.insert((p*p - bottom - 1) / 2)
    p := p + 2
  for i from 0 to m - 1
    for j from qs[i] to delta step ps[i]
      sieve[j] := False
  return m, ps, qs, sieve

这里的ps是当前最大值以下筛选质数列表,而qs是对应ps在当前段中最小倍数的偏移量。 advance函数清除位阵列,重置qs,扩展psqs以包含新的筛选质数,然后筛选下一个区间。

function genPrimes()
  bottom, i, delta := 0, 1, 50000
  m, ps, qs, sieve := init(delta)
  yield 2
  while True
    if i == delta # reset for next segment
      i, bottom := -1, bottom + 2 * delta
      m, ps, qs, sieve := \textbackslash
        advance(m, ps, qs, sieve, bottom, delta)
    else if sieve[i] # found prime
      yield bottom + 2*i + 1
    i := i + 1

节段大小 2 * delta 被任意设置为100000。这种方法需要O(sqrt(n))的筛选素数空间以及筛子的常数空间。

使用轮子生成候选数并测试其是否为质数速度较慢,但可以节省空间。

function genPrimes()
  w, wheel := 0, [1,2,2,4,2,4,2,4,6,2,6,4,2,4, \
       6,6,2,6,4,2,6,4,6,8,4,2,4,2,4,8,6,4,6, \
       2,4,6,2,6,6,4,2,4,6,2,6,4,2,4,2,10,2,10]
  p := 2; yield p
  repeat
    p := p + wheel[w]
    if w == 51 then w := 4 else w := w + 1
    if isPrime(p) yield p

在筛选范围变大时,最好先使用筛子,当筛子变得太大时再切换到轮子。更好的方法是继续使用一组固定的筛素数,一旦该组变得太大,然后仅报告通过素数测试的那些值 bottom + 2*i + 1


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