为什么我的Atkin筛的实施比Eratosthenes慢?

时间:2011-06-30 16:38:49

标签: ruby algorithm primes

我正在使用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

3 个答案:

答案 0 :(得分:5)

作为Wikipedia says,“阿特金的现代筛子更复杂,但在适当优化时更快”(我的重点)。

在第一组循环中节省一些时间的第一个显而易见的地方是当y大于4*x**2 + y**2时停止迭代n。例如,如果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。相反,您可以循环遍历{的正确值{仅限{1}},并避免完全测试余数。

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


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

答案 1 :(得分:5)

如果您实际上首先正确实施了Eratosthenes的Sieve,那将会有很大帮助。

该筛子的关键特征是每次只能对一个数字进行一次操作。相比之下,你正在为每个素数工作而不是数量。差异很微妙,但性能影响巨大。

以下是您未能实施的实际筛选:

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问题都应该足够快。

答案 2 :(得分:1)

@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)。