NumPy 06-Python实验六-4 埃拉托斯特尼筛法-Numpy.py

埃拉托斯特尼筛法(Sieve of Eratosthenes,简称埃氏筛)

✅ 核心思想

要找出不大于 N 的全部素数:先假设所有数都是素数,然后不断把已知素数的倍数标记为 “不是素数”,筛完剩下未被标记的就是素数。

素数定义:大于 1,只能被 1 和自身整除的整数。


📜 手工演示(举例:N=10,找出 ≤10 的素数)

数字列表:2, 3, 4, 5, 6, 7, 8, 9, 10

  1. 取第一个素数 2:划掉所有 2 的倍数 → 4、6、8、10 剩下:2, 3, 5, 7, 9
  2. 取下一个没划掉的数 3:划掉 3 的倍数 →9 剩下:2,3,5,7
  3. 下一个数是 5。\(5>\sqrt{10}\approx3.16\),停止循环 最终素数:2,3,5,7

关键结论:外层循环只需要到 \(\boldsymbol{\sqrt{N}}\)。 任何合数x,一定存在一个因子 \(\le\sqrt{x}\);如果遍历到\(\sqrt{N}\)还没筛掉 x,x 一定是素数。

 

 

# 小于200万的素数之和:

import time
import numpy as np
start=time.time()

n=2000000
prime=np.arange(n+1,dtype=np.uint64)
r=int(np.sqrt(n))

for j in range(2,r+1):
    if prime[j] != 0:
        prime[j*j:n+1:j]=0
print(np.int64(prime.sum()-1))

end=time.time()
print("time:",end-start)

# 142913828922
# time: 0.10690665245056152

 

代码逐行详细讲解

算法:埃拉托斯特尼筛法(埃氏筛,Sieve of Eratosthenes) 目标:求出200 万以内所有素数的总和,用 NumPy 数组加速筛法,比纯 Python 列表循环快很多。 答案结果:142913828922

import time
import numpy as np
  • import time:计时,统计程序运行耗时
  • import numpy as np:使用 numpy 数组做筛子,利用向量化赋值提速
start=time.time()

记录程序开始的时间戳(秒)。

n=2000000

上限:我们要找 小于等于 2000000 的所有素数。

prime=np.arange(n+1,dtype=np.uint64)
  • np.arange(n+1):生成数组 [0,1,2,3,4,...,2000000]
  • dtype=np.uint64:无符号 64 位整数,防止大数求和溢出

这个数组prime就是筛子: 初始:prime[i] = i 后面如果判定i不是素数,就把prime[i]置为0; 最后数组里不为 0 的元素,就是素数。

r=int(np.sqrt(n))

\(\sqrt{2000000}\approx1414.21\),取整 r=1414。

埃氏筛原理:只需要筛到\(\sqrt{n}\)即可。 如果一个数n存在大于\(\sqrt{n}\)的因子,那必然对应一个小于\(\sqrt{n}\)的因子,所以外层循环只需要到\(\sqrt{n}\)。

for j in range(2,r+1):
    if prime[j] != 0:
        prime[j*j:n+1:j]=0

外层循环,j从 2 遍历到 1414:

  1. if prime[j] != 0:说明j还没有被置 0 → j 是素数
  2. prime[j*j : n+1 : j] = 0
    • 切片:从 \(j^2\) 开始,到 n 为止,步长 j
    • 含义:把 j 的所有倍数(\(j^2,j^2+j,j^2+2j\cdots\))全部标记为 0(非素数)

为什么从j*j开始,不从2*j? 小于\(j^2\)的 j 倍数,早就被更小的素数筛掉了,直接从\(j^2\)起步减少重复工作,优化速度。

✨关键点:prime[...] = 0 是NumPy 向量化批量赋值,不是 Python for 循环逐个赋值,这是代码速度很快的核心原因!

print(np.int64(prime.sum()-1))
  • prime.sum():数组全部元素求和。
  • 数组里prime[0]=0,prime[1]=1。1 不是素数,但初始数组 prime [1]=1,会被算进总和,所以减去 1。

举例:数组元素[0,1,2,3,0,5,0,7...],sum 包含了 1,素数是 2,3,5,7,总和 = sum-1。 np.int64:转换类型,防止 uint64 输出格式问题。

end=time.time()
print("time:",end-start)

记录结束时间,计算耗时并打印。

算法整体流程总结

  1. 创建数组 0~2000000,数组下标代表数字本身;
  2. j 从 2 到√2000000 循环;
  3. 如果 j 是素数,就把 j² 之后所有 j 倍数全部标记为 0;
  4. 筛完之后,数组中非 0 的数字就是素数;
  5. 全部相加,减去多余的 1,得到所有素数之和。

重点细节 & 坑点

  1. 为什么用uint64? 200 万素数总和非常大(1429 亿),普通 32 位 int 存不下,会整数溢出,所以用 64 位整型。
  2. 筛法起点j*j优化: 朴素写法会写成从2*j开始,结果正确但是慢;j*j是经典优化。
  3. NumPy 切片赋值是向量化操作 prime[j*j:n+1:j]=0,底层 C 批量赋值,没有 Python 内部循环,这就是为什么 0.1 秒就能跑完 200 万。纯 Python 列表实现埃氏筛,耗时会高很多。
  4. 必须sum()-1 prime [1]=1,1 不是素数,但没有被筛掉,求和时需要减掉。prime [0]=0 不影响。

简单手写小例子(n=10,方便理解筛法)

初始prime = [0,1,2,3,4,5,6,7,8,9,10]
r = sqrt(10)=3
j=2,prime[2]≠0:prime[4::2]=0 → 数组变为[0,1,2,3,0,5,0,7,0,9,0]
j=3,prime[3]≠0:prime[9::3]=0 → 数组变为[0,1,2,3,0,5,0,7,0,0,0]
sum = 0+1+2+3+0+5+0+7+0+0+0 =18
sum-1 =17
素数:2,3,5,7 总和=17 ✔

对比:原生 Python 列表版本(速度慢很多)

import time
n=2000000
prime = [1]*(n+1)
prime[0]=prime[1]=0
r=int(n**0.5)
start=time.time()
for j in range(2,r+1):
    if prime[j]:
        for k in range(j*j,n+1,j):
            prime[k]=0
s=sum(i for i, is_p in enumerate(prime) if is_p)
print(s)
print(time.time()-start)

原生版本内层是 Python 循环,耗时通常在 1 秒级别,比 numpy 版本慢十倍左右。

 

posted @ 2026-10-21 10:18  emanlee  阅读(3)  评论(0)    收藏  举报