【问题标题】:Why is my implementation of Atkin sieve is slower than Eratosthenes? [closed]为什么我的 Atkin 筛子的实现比 Eratosthenes 慢?
【发布时间】:2011-09-26 03:46:34
【问题描述】:

我正在用 Ruby 解决 Project Euler 的问题,并实现了 Atkin 的筛子来寻找素数,但它的运行速度比 Eratosthenes 的筛子慢。有什么问题?

def atkin_sieve(n)
  primes = [2,3,5]

  sieve = Array.new(n+1, false)

  y_upper = n-4 > 0 ? Math.sqrt(n-4).truncate : 1
  for x in (1..Math.sqrt(n/4).truncate) 
    for y in (1..y_upper)
      k = 4*x**2 + y**2
      sieve[k] = !sieve[k] if k%12 == 1 or k%12 == 5
    end
  end

  y_upper = n-3 > 0 ? Math.sqrt(n-3).truncate : 1
  for x in (1..Math.sqrt(n/3).truncate)
    for y in (1..y_upper)
      k = 3*x**2 + y**2
      sieve[k] = !sieve[k] if k%12 == 7
    end
  end

  for x in (1..Math.sqrt(n).truncate)
    for y in (1..x)
      k = 3*x**2 - y**2
      if k < n and k%12 == 11
    sieve[k] = !sieve[k]
      end
    end
  end

  for j in (5...n)
    if sieve[j]
      prime = true
      for i in (0...primes.length)
        if j % (primes[i]**2) == 0
          prime = false
          break
        end
      end
      primes << j if prime
    end
  end
  primes
end

def erato_sieve(n)
  primes = []
  for i in (2..n)
    if primes.all?{|x| i % x != 0}
      primes << i
    end
  end
  primes
end

【问题讨论】:

标签: ruby algorithm primes


【解决方案1】:

正如Wikipedia says,“Atkin 的现代筛子更复杂,但在适当优化后速度更快”(我的重点)。

在第一组循环中节省一些时间的第一个明显的地方是当4*x**2 + y**2 大于n 时停止迭代y。例如,如果 n 是 1,000,000 并且 x 是 450,那么当 y 大于 435 时应该停止迭代(而不是像现在那样继续迭代到 999)。所以你可以将第一个循环重写为:

for x in (1..Math.sqrt(n/4).truncate)
    X = 4 * x ** 2
    for y in (1..Math.sqrt(n - X).truncate)
        k = X + y ** 2
        sieve[k] = !sieve[k] if k%12 == 1 or k%12 == 5
    end
end

(这也避免了每次循环都重新计算4*x**2,尽管这可能是一个非常小的改进,如果有的话。)

当然,类似的评论也适用于y 上的其他循环。


您可以加快速度的第二个地方是循环y 的策略。您循环遍历该范围内的所有y 值,然后检查哪些值导致k 的值具有正确的余数模12。相反,您可以只循环遍历y 的正确值,并避免完全测试剩余部分。

如果 4*x**2 是 4 模 12,则 y**2 必须是 1 或 9 模 12,因此 y 必须是 1、3、5、7 或 11 模 12。如果 4*x**2 是 8模 12,那么 y**2 必须是 5 或 9 模 12,所以 y 必须是 3 或 9 模 12。最后,如果 4*x**2 是 0 模 12,那么 y**2 必须是 1 或 5 模 12 ,所以y 必须是 1、5、7、9 或 11 模 12。


我还注意到,您的 Eratosthenes 筛子正在通过测试 i 以下所有素数的可分性来做无用的工作。一旦您测试了所有小于或等于i 平方根的素数的整除性,您就可以停止迭代。

【讨论】:

  • 关于优化阿特金筛的实现还有更多问题:请参阅new edited Wikipedia article for new pseudo code。与使用模 60 相比,使用模 12 简化增加了大约 23% 的工作量,并且模测试拒绝了几乎一半的计算,这可以通过添加一个中间循环来测试模序列的每个起始编号来纠正,然后通过数组复制序列。最后,对于实际范围,SoA 永远不会比最大轮子分解的 SoE 快。
【解决方案2】:

如果您一开始就正确实施了埃拉托色尼筛法,那将会有很大帮助。

该筛子的关键特征是每次质数除以数字时您只执行一次操作。相比之下,您正在为每个小于该数字的素数做功。差异是微妙的,但对性能的影响是巨大的。

这是您未能实施的实际筛子:

def eratosthenes_primes(n)
    primes = []
    could_be_prime = (0..n).map{|i| true}
    could_be_prime[0] = false
    could_be_prime[1] = false
    i = 0
    while i*i <= n
        if could_be_prime[i]
            j = i*i
            while j <= n
                could_be_prime[j] = false
                j += i
            end
        end
        i += 1
    end
    return (2..n).find_all{|i| could_be_prime[i]}
end

将此与您的代码进行比较,以找出最多为 50,000 的所有素数。另请注意,通过特殊封装偶数的逻辑,这可以很容易地加快 2 倍。有了这个调整,这个算法对于需要计算大量素数的每个 Project Euler 问题应该足够快。

【讨论】:

  • 是的。每个组合仅由其素因子生成(低于其sqrt),但每个候选者由所有前面的素数(低于其sqrt)测试 )。
【解决方案3】:

@Gareth 提到了一些关于 4x^2+y^2 的冗余计算。在此处和在循环中进行计算的其他地方,您都可以利用已经执行的计算并将其简化为简单的加法。

而不是X=4 * x ** 2,您可以依赖X 已经具有4 * (x-1) ** 2 的值这一事实。由于 4x^2 = 4(x-1)^2 + 4(2x - 1),您只需将 8 * x - 4 添加到 X。您可以对k 和其他需要重复计算的地方(如 3x^2 + y^2)使用相同的技巧。

【讨论】:

  • 曾几何时,这种微优化很重要,但我认为即使在像 C 这样的语言中,现在也没什么区别,因为大多数处理器都可以启动(在某些情况下,完成) 每个周期都有一个新的乘法。在 Ruby 等解释型语言中,解释开销使您可能通过这种更改节省的任何费用都相形见绌。
  • 特别是,您在这里建议用两个加法替换乘法。这完全有可能让事情变慢!
猜你喜欢
  • 2012-11-07
  • 2018-02-11
  • 1970-01-01
  • 1970-01-01
  • 2014-03-14
  • 2016-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多