【问题标题】:Sum of primes below 2,000,000 in pythonpython中低于2,000,000的素数之和
【发布时间】:2015-12-09 02:41:13
【问题描述】:

我正在尝试 Project Euler 的第 10 题,它是所有低于 2,000,000 的素数的总和。我曾尝试使用 Python 实现 Erasthotenes 筛,并且我编写的代码非常适用于 10,000 以下的数字。

但是,当我试图找到更大数字的素数总和时,代码运行时间太长(找到高达 100,000 的素数总和需要 315 秒)。该算法显然需要优化。

是的,我看过这个网站上的其他帖子,比如Fastest way to list all primes below N,但是那里的解决方案对代码的工作原理几乎没有解释(我仍然是初学者)所以我无法真正学习来自他们。

有人可以帮我优化我的代码,并清楚地解释它是如何工作的吗?

这是我的代码:

primes_below_number = 2000000 # number to find summation of all primes below number
numbers = (range(1, primes_below_number + 1, 2)) # creates a list excluding even numbers
pos = 0 # index position
sum_of_primes = 0 # total sum
number = numbers[pos]
while number < primes_below_number and pos < len(numbers) - 1:
    pos += 1
    number = numbers[pos] # moves to next prime in list numbers
    sum_of_primes += number # adds prime to total sum
    num = number
    while num < primes_below_number:
        num += number
        if num in numbers[:]:
            numbers.remove(num) # removes multiples of prime found

print sum_of_primes + 2

正如我之前所说,我是编程新手,因此对任何复杂概念的彻底解释将不胜感激。谢谢。

【问题讨论】:

  • 您在测试 num 时创建numbers[:] 的副本的任何原因?
  • 从您正在迭代的列表中删除项目不安全的,但是您的代码没有这样做,因此正如 champion 所指出的那样,您不需要在此处复制numbers

标签: python optimization primes


【解决方案1】:

如您所见,有多种方法可以在 Python 中实现 Erasthotenes 筛,这些方法比您的代码更有效。我不想用花哨的代码混淆你,但我可以展示如何加快你的代码速度。

首先,搜索列表并不快,从列表中删除元素更慢。然而,Python 提供了一种集合类型,它在执行这两种操作时非常有效(尽管它确实比简单的列表消耗更多的 RAM)。令人高兴的是,修改代码以使用集合而不是列表很容易。

另一个优化是我们不必一直检查直到primes_below_number 的素因数,我在下面的代码中将其重命名为hi。只求hi 的平方根就足够了,因为如果一个数是合数,它的因数必须小于或等于它的平方根。

我们不需要保留素数总和的运行总和。最好在最后使用 Python 内置的 sum() 函数,该函数以 C 速度运行,因此比以 Python 速度逐一加法要快得多。

# number to find summation of all primes below number
hi = 2000000

# create a set excluding even numbers
numbers = set(xrange(3, hi + 1, 2)) 

for number in xrange(3, int(hi ** 0.5) + 1):
    if number not in numbers:
        #number must have been removed because it has a prime factor
        continue

    num = number
    while num < hi:
        num += number
        if num in numbers:
            # Remove multiples of prime found
            numbers.remove(num)

print 2 + sum(numbers)

您应该会发现这段代码在几秒钟内运行;在我的 2GHz 单核机器上大约需要 5 秒。

您会注意到我已经移动了 cmets,使它们位于他们正在评论的行的上方。这是 Python 中的首选样式,因为我们更喜欢短行,而且内联 cmets 往往会使代码看起来杂乱无章。

还有一个可以对内部while 循环进行的小优化,但我让您自己弄清楚。 :)

【讨论】:

  • 一个密集集最好用位图(真或假数组)表示,而不是存储实际整数的集合,而不是每个位置的布尔值。无论是基于散列的集合,还是像使用的 OP 这样的平面数组,都是如此。
  • @PeterCordes:当然!但是这个答案的重点是展示如何通过使用更适合搜索和删除的数据类型来提高 OP 算法的速度。在我自己的 Python 素数筛子中,我倾向于使用布尔值列表,正如您在 saulspatz 答案的评论中的链接中看到的那样。
  • FWIW,在我的 C 素数筛子中,我使用的位图利用了这样一个事实,即所有素数 > 30 都与 {1、7、11、13、17、19、23 之一的模 30 一致, 29},所以位图中的每个字节处理一个由 30 个整数组成的块。寻址这样的位图比简单的位图稍慢,但节省空间值得。
【解决方案2】:

首先,从列表中删除数字会很慢。而不是这个,做一个列表

primes = primes_below_number * True
primes[0] = False
primes[1] = False

现在在你的循环中,当你找到一个素数 p 时,将所有合适的 k 更改为 primes[k*p]False。 (你实际上不会做乘法,你会不断地加上 p,当然。)

最后,

primes = [n for n i range(primes_below_number) if primes[n]]

这应该会快很多。

其次,一旦找到大于primes_below_number 平方根的素数,您就可以停止查找,因为合数必须有一个不超过其平方根的素数。

【讨论】:

  • FWIW,如果您使用扩展切片符号like this,这种方法会相当快。并且该过程可以适应查找primes in a range
【解决方案3】:

尝试使用 numpy,应该会更快。将 range 替换为 xrange,它可能会对您有所帮助。

【讨论】:

  • OP 采用 len 的数字,所以 xrange 会打破它。
【解决方案4】:

这是对您的代码的优化:

import itertools

primes_below_number = 2000000
numbers = list(range(3, primes_below_number, 2))
pos = 0

while pos < len(numbers) - 1:
    number = numbers[pos]
    numbers = list(
        itertools.chain(
            itertools.islice(numbers, 0, pos + 1),
            itertools.ifilter(
                lambda n: n % number != 0,
                itertools.islice(numbers, pos + 1, len(numbers))
            )
        )
    )
    pos += 1

sum_of_primes = sum(numbers) + 2
print sum_of_primes

这里的优化是因为:

  1. 将总和移到循环之外。
  2. 我们可以创建另一个列表,而不是从列表中删除元素,内存在这里不是问题(我希望)。
  3. 在创建新列表时,我们通过链接两个部分来创建它,第一部分是当前数字之前的所有内容(我们已经检查了这些),第二部分是当前数字之后的所有内容,但前提是它们不能被当前号码。
  4. 使用itertools 可以使事情变得更快,因为我们将使用迭代器而不是多次循环整个列表。

另一种解决方案是不删除列表的某些部分,而是像 @saulspatz 所说的那样禁用它们。

这是我能找到的最快方法:http://www.wolframalpha.com/input/?i=sum+of+all+primes+below+2+million?

更新

这里是布尔方法:

import itertools

primes_below_number = 2000000
numbers = [v % 2 != 0 for v in xrange(primes_below_number)]
numbers[0] = False
numbers[1] = False
numbers[2] = True

number = 3

while number < primes_below_number:
    n = number * 3  # We already excluded even numbers
    while n < primes_below_number:
        numbers[n] = False
        n += number
    number += 1
    while number < primes_below_number and not numbers[number]:
        number += 1

sum_of_numbers = sum(itertools.imap(lambda index_n: index_n[1] and index_n[0] or 0, enumerate(numbers)))

print(sum_of_numbers)

这在几秒钟内执行(在我的 2.4GHz 机器上花了 3 秒钟)。

【讨论】:

  • 对此有一个简单的优化,您可以进行切片分配而不是第一个内部循环:numbers[2*number-1::number] = [False] * (primes_below_number//number-1),并在第二个内部循环number += 2 中增加 2。在我的机器上,这减少了 40% 的时间。
【解决方案5】:

您可以存储一个布尔值数组,而不是存储数字列表。位图的这种使用可以被认为是实现集合的一种方式,它适用于密集集合(成员的值之间没有大的差距)。

An answer on a recent python sieve question 使用此实现 python 样式。事实证明,很多人已经实现了筛子,或者他们认为是筛子的东西,然后来问为什么它很慢。 :P 如果您需要更多阅读材料,请查看其中一些相关问题的侧边栏。

查找包含表示数字是否在集合中的布尔值的元素非常简单且非常快速。 array[i] 是一个布尔值,如果 i 在集合中,则为 true,否则为 false。内存地址可以直接从i 计算,只需一次加法即可。

(我在掩饰这样一个事实,即一个布尔数组可能会为每个元素存储一个完整的字节,而不是更有效地实现将每个位用于不同的元素。任何像样的筛子都将使用位图.)

从集合中删除一个数字就像设置 array[i] = false 一样简单,不管之前的值是什么。没有搜索,没有比较,没有跟踪发生了什么,只是一次记忆操作。 (好吧,两个位图:加载旧字节,清除正确的位,存储它。内存是字节寻址的,但不是位寻址的。)


基于位图的筛选器的一个简单优化是甚至不存储偶数字节,因为只有一个偶数素数,我们可以对其进行特殊处理以使内存密度加倍。然后i 的会员状态保存在array[i/2] 中。 (除以 2 的幂对计算机来说很容易。其他值要慢得多。)

一个 SO 问题: Why is Sieve of Eratosthenes more efficient than the simple "dumb" algorithm? 有很多关于筛子的好东西的链接。 This one in particular 有一些关于它的很好的讨论,用文字而不是代码。 (没关系,它谈论的是一个常见的 Haskell 实现,看起来像一个筛子,但实际上不是。他们在他们的图表中称其为“不忠实”的筛子,等等。)

discussion on that question 指出,对于某些用途,试除法可能比大筛子快,因为清除每个素数的所有倍数的位会以缓存不友好的模式触及大量内存。如今,CPU 比内存快得多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-11
    • 1970-01-01
    • 1970-01-01
    • 2014-12-31
    • 1970-01-01
    • 2012-08-04
    相关资源
    最近更新 更多