The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17.
Find the sum of all the primes below two million.
质数求和
所有小于 10 的质数的和是 2 + 3 + 5 + 7 = 17。
求所有小于两百万的质数的和。
我们可以使用在 problem 7 中写过的素数判定函数:
(defun eu7-isprime (n)
(cond
((<= n 1) nil)
((< n 4) t)
((zerop (% n 2)) nil)
((< n 9) t)
((zerop (% n 3)) nil)
(t (let ((bound (floor (sqrt n))))
(named-let f ((i 5))
(cond
((> i bound) t)
((zerop (% n i)) nil)
((zerop (% n (+ i 2))) nil)
(t (f (+ i 6)))))))))
(named-let f ((n 3) (sum 2))
(cond
((>= n 2000000) sum)
(t (if (eu7-isprime n)
(f (+ n 2) (+ sum n))
(f (+ n 2) sum)))))
;; 142913828922
对于这个问题来说上面的代码是够用的,但是整个过程的复杂度大概是 O(n1.5) (isprime 函数的复杂度大概是 O(n0.5))。下面我们用埃氏筛实现一下,它的复杂度是 O(nlog(n)log(n)),整个算法的步骤大概是这样:
- Make a list of all numbers from 2 to N.
- Find the next number p not yet crossed out. This is a prime. If it is greater than √N, go to 5.
- Cross out all multiples of p which are not yet crossed out.
- Go to 2.
- The numbers not crossed out are the primes not exceeding N.
(defun eu10-sieve-of-Era (n)
(let ((limit (floor (sqrt n)))
(vec (make-bool-vector (1+ n) t)))
(aset vec 0 nil)
(aset vec 1 nil)
(cl-loop for i from 4 to n by 2
do (aset vec i nil))
(cl-loop for i from 3 to limit by 2
do (when (aref vec i)
(cl-loop for m from (* i i) to n by (* i 2)
do (aset vec m nil))))
vec))
(cl-loop for a across (eu10-sieve-of-Era 2000000)
for i from 0
sum (if a i 0))
;; 142913828922
由于除了 2 以外的所有偶数都不可能是素数,我们完全可以忽略它们而只处理奇数,现在我们建立如下索引的数组:
0, 1, 2, 3, 4,…
1, 3, 5, 7, 9,…
将上面的数字乘二加一,我们就可以得到所有的奇数了。对于给定的范围 N,我们需要的数组大小就是 (N+1)/2。比如对于 1000 或 999,我们需要的奇数就是 500 个(当然如果我们忽略掉 1 的话,那我们就只需要 (N-1)/2 个了,为了让索引与自然数对应,这里我们还是取 (N+1)/2,然后把 1 留个 0)。
现在让我们来考虑索引问题,现在 1 对应于 3,2 对应于 5,假设我们碰到了某个素数,我们要使用它筛掉它的倍数,那么步进值应该取多少?首先假设这个索引是 i,那么对应的数字就是 2×i+1,(2*i+1)2=4×i2+4×i+1,将这个减一除二就可以得到它在数组中的索引:2(i2+i)。在上面的代码中步进值是 2×i,那么这里应该就是 i(全是奇数,不用跳过偶数了),然后替换为 2×i+1,我们就得到了步进值。下面是实现代码:
(defun eu10-sieve-of-Era2 (n)
(let* ((bound (/ (1+ n) 2))
(vec (make-bool-vector bound t))
(crosslimit (/ (floor (sqrt n)) 2)))
(aset vec 0 nil)
(cl-loop for i from 1 to crosslimit
do (when (aref vec i)
(cl-loop for j from (* 2 i (1+ i))
to (1- bound) by (1+ (* i 2))
do (aset vec j nil))))
vec))
(+ 2 (cl-loop
for a across (eu10-sieve-of-Era2 2000000)
for i from 0
sum (if a (1+ (* i 2)) 0)))
;; 142913828922
理论上这能快一倍,不过实际可能要慢一会儿。
除了埃氏筛法外还有线性的欧拉筛法,可以参考这篇文章以及它的中文译文:
考虑到文章本身使用 CC-BY-SA 4.0,因此我可以在此处适当转载和演绎,以免之后链接失效丢失内容。
如你所见,上面的代码中外层循环的范围并不是整个数组的长度,而是范围的平方根。这也就说明就筛选目的来说,我们只需要保留到 √n 的素数就足够了。因为本题的目的是求二百万以下的素数之和,如果我们将 [0, 2000000] 这个区间分块,然后按循环的思路分别筛取块内的素数然后加起来的话,占用的内存将会大大减少(每次循环之后使用的区块就没用了,也就是说只需要筛子占用的内存和单个区块的内存即可,而不需要长度为二百万这样大的数组)。
设 s 是块的大小,那么块的数量就是 floor(n/s)
,块 k( k = 0,1, …) 包含了区间 [ks, ks + s -1] 中的数字。一般来说块的大小取 104 到 105 之间。如果块太小会让循环次数太大,如果块太大可能不能很好地匹配 CPU 的缓存。
(defun eu10-sieve-of-Era3 (n)
(let* ((s 90000)
(sum -1)
(limit (floor (sqrt n)))
(primes nil)
(vec (make-bool-vector (1+ limit) t))
(block (make-bool-vector s t)))
(aset vec 0 nil)
(aset vec 1 nil)
(cl-loop
for i from 4 to limit by 2
do (when (aref vec i)
(cl-loop
for j from (* i i) to limit by i
do (aset vec j nil))))
(setq primes (nreverse primes))
(cl-loop
for i from 0 to limit
do (when (aref vec i) (push i primes)))
(cl-do ((k 0 (1+ k))) ((> (* k s) n) sum)
(fillarray block t)
(let ((start (* k s)))
(cl-loop
for p in primes
do (let* ((start-index (/ (+ start p -1) p)))
(cl-do ((j (- (* (max start-index p) p) start)
(+ j p)))
((>= j s))
(aset block j nil))))
(cl-do ((i 0 (1+ i)))
((or (>= i s) (> (+ i start) n)))
(when (aref block i) (cl-incf sum (+ i start))))))))
(eu10-sieve-of-Era3 2000000)
;; 142913828922
同样,这个实现也可以通过去掉偶数来进一步改进。
在埃氏筛中某个合数可能被重复多次标记,因此它的复杂度并不是 O(n),如果让每个合数只被标记一次,那么时间复杂度就是 O(n) 了,这也叫做欧拉筛。
(defun eu10-euler (n)
(let* ((vec (make-bool-vector (1+ n) t))
(primes (list 0))
(primes-tail primes)
(sum 0))
(aset vec 0 nil)
(aset vec 1 nil)
(cl-do ((i 2 (1+ i))) ((> i n) sum)
(when (aref vec i)
(cl-incf sum i)
(setcdr primes-tail (list i))
(setq primes-tail (cdr primes-tail)))
(let ((pri (cdr primes))
(flag t)
tmp)
(while (and flag (setq tmp (pop pri)))
(cond
((> (* tmp i) n) (setq flag nil))
(t (aset vec (* i tmp) nil)
(when (zerop (% i tmp))
(setq flag nil)))))))))
(eu10-euler 2000000)
;; 142913828922
关于欧拉筛的原理,此处就不介绍了,读者可以参考其他的更详细的文章。