Project Euler中的第10个问题:
低于10的素数之和为2 + 3 + 5 + 7 = 17.
找出200万以下所有素数的总和。
我找到了这个片段:
sieve = [True] * 2000000 # Sieve is faster for 2M primes
def mark(sieve, x):
for i in xrange(x+x, len(sieve), x):
sieve[i] = False
for x in xrange(2, int(len(sieve) ** 0.5) + 1):
if sieve[x]: mark(sieve, x)
print sum(i for i in xrange(2, len(sieve)) if sieve[i])
发布here 运行3秒钟。
我写了这段代码:
def isprime(n):
for x in xrange(3, int(n**0.5)+1):
if n % x == 0:
return False
return True
sum=0;
for i in xrange(1,int(2e6),2):
if isprime(i):
sum += i
我不明白为什么我的代码(第二个)慢得多?
答案 0 :(得分:10)
您的算法分别从2到N(其中N = 2000000)检查每个数字的原始性。
Snippet-1使用大约2200年前发现的 sieve of Eratosthenes 算法。 它不会检查每个数字,但是:
因此算法产生的所有素数都达到N.
请注意,它不进行任何除法,只进行加法(甚至不是乘法,而不是因为数字如此之小而重要,但可能会有更大的数字)。时间复杂度为O(n loglogn)
,而您的算法接近O(n^(3/2))
(或@ O(n^(3/2) / logn)
为@Daniel Fischer评论),假设分割成本与乘法相同。
来自维基百科(上面链接)的文章:
随机访问机器模型的时间复杂度是O(n log log n)运算,这是prime harmonic series渐近逼近
log log n
这一事实的直接后果。
(在这种情况下为n = 2e6
)
答案 1 :(得分:4)
第一个版本预先计算范围内的所有素数并将它们存储在sieve
数组中,然后找到解决方案就是在数组中添加素数的简单问题。它可以被视为memoization的一种形式。
第二个版本测试范围内的每个数字,以查看它是否为素数,重复先前计算已经完成的大量工作。
总之,第一个版本避免重新计算值,而第二个版本一次又一次地执行相同的操作。
答案 2 :(得分:2)
为了便于理解差异,请尝试考虑将每个数字用作潜在分隔符的次数:
在您的解决方案中,当该数字作为素数进行测试时,将测试数字2的每个数字。您沿途传递的每个号码都将用作下一个号码的潜在分隔符。
在第一个解决方案中,一旦你跨过一个你永远不会回头的数字 - 你总是从你到达的地方向前移动。顺便说一句,一个可能的常见优化是只有在你标记为2之后才能获得奇数:
mark(sieve, 2)
for x in xrange(3, int(len(sieve) ** 0.5) + 1, 2):
if sieve[x]: mark(sieve, x)
通过这种方式,您只需查看每个数字并清除其所有乘法,而不是一次又一次地检查所有可能的分频器,并检查每个数字及其所有前一个数字,而if
语句会阻止您为你以前遇到的号码重复工作。
答案 3 :(得分:2)
正如Óscar的回答所示,你的算法重复了很多工作。要查看其他算法保存的处理量,请考虑mark()
和isprime()
函数的以下修改版本,这些函数会跟踪调用函数的次数以及for循环的总次数迭代:
calls, count = 0, 0
def mark(sieve, x):
global calls, count
calls += 1
for i in xrange(x+x, len(sieve), x):
count += 1
sieve[i] = False
使用这个新函数运行第一个代码后,我们可以看到mark()
被调用223次,for循环中总共有4,489,006(~450万)次迭代。
calls, count = 0
def isprime(n):
global calls, count
calls += 1
for x in xrange(3, int(n**0.5)+1):
count += 1
if n % x == 0:
return False
return True
如果我们对您的代码进行类似的更改,我们可以看到isprime()
被称为1,000,000(100万)次,其中for循环的177,492,735(~177.5百万)次迭代。
计算函数调用和循环迭代并不总是确定算法为什么更快的决定性方法,但通常更少的步骤= =更少的时间,并且显然您的代码可以使用一些优化来减少步骤数。