204. 计数质数 - 力扣(LeetCode)
2521. 数组乘积中的不同质因数数目 - 力扣(LeetCode)
参考文档: 分享丨【算法题单】数学算法(数论/组合/概率期望/博弈/计算几何/随机算法) - 讨论 - 力扣(LeetCode)
埃式筛选法
埃拉托斯特尼筛法(简称 "埃式筛法")是一种高效查找一定范围内所有质数 的经典算法,由古希腊数学家埃拉托斯特尼提出。其核心思想是:通过排除已知质数的所有倍数,剩下的未被排除的数就是质数。
一、算法原理(步骤)
以 "找出小于等于 n
的所有质数" 为例,步骤如下:
-
初始化标记数组 :创建一个长度为
n+1
的布尔数组is_prime
,其中is_prime[i]
表示 "数字i
是否为质数"。- 初始时假设所有数字都是质数,即
is_prime[i] = True
(0
和1
除外,它们不是质数,直接设为False
)。
- 初始时假设所有数字都是质数,即
-
筛选过程 :从第一个质数
2
开始,依次遍历每个数i
:- 若
is_prime[i]
为True
(说明i
是质数),则将i
的所有倍数(从i*i
开始,或从2*i
开始)标记为False
(因为它们是合数)。 - (优化点:从
i*i
开始标记,因为i*2, i*3, ..., i*(i-1)
已经被比i
小的质数标记过了。例如i=5
时,5*2=10
已被2
标记,5*3=15
已被3
标记,只需从5*5=25
开始标记)。
- 若
-
终止条件 :遍历到
√n
即可停止。因为大于√n
的合数,其因子一定小于√n
,早已被标记过。 -
结果提取 :最终
is_prime
中值为True
的索引,就是小于等于n
的所有质数。
二、示例:筛选 30 以内的质数
- 初始化
is_prime[0..30] = [False, False, True, True, ..., True]
(0 和 1 为 False,其余为 True)。 - 从
i=2
开始:i=2
是质数,标记4,6,8,...,30
为False
。
- 下一个
i=3
(仍为 True):- 标记
9,12,15,...,30
为False
(6、12 等已被 2 标记,不影响)。
- 标记
- 下一个
i=4
(已被标记为 False,跳过)。 - 下一个
i=5
(仍为 True):- 标记
25, 30
为False
(10、15 等已被标记)。
- 标记
- 继续直到
i=√30≈5.47
,遍历到i=5
即可停止。 - 剩余
is_prime[i]=True
的i
为:2,3,5,7,11,13,17,19,23,29
,即 30 以内的所有质数。
三、代码实现(Python)
下面是埃式筛法的 Python 实现,用于查找 n
以内的所有质数:
def sieve_of_eratosthenes(n):
if n < 2:
return [] # 小于2的数没有质数
# 初始化:假设所有数都是质数(0和1除外)
is_prime = [True] * (n + 1)
is_prime[0], is_prime[1] = False, False # 0和1不是质数
# 从2遍历到√n(取整数部分)
for i in range(2, int(n ** 0.5) + 1):
if is_prime[i]: # 若i是质数,则标记其倍数为非质数
# 从i*i开始标记(优化),步长为i
for j in range(i * i, n + 1, i):
is_prime[j] = False
# 提取所有质数(is_prime[i]为True的索引)
primes = [i for i, prime in enumerate(is_prime) if prime]
return primes
# 示例:找出100以内的质数
if __name__ == "__main__":
n = 100
primes = sieve_of_eratosthenes(n)
print(f"{n}以内的质数有:{primes}")
四、算法特点
- 时间复杂度 :
O(n log log n)
,效率远高于 "逐个判断是否为质数" 的方法(后者为O(n√n)
)。 - 空间复杂度 :
O(n)
,需要一个长度为n+1
的数组存储标记。 - 适用场景 :需要一次性找出 "较小范围内所有质数" 的场景(如
n
在10^6
级别内效率极高;若n
过大,可能需要优化空间的 "分段筛法")。
通过这种 "标记倍数" 的方式,埃式筛法巧妙地避免了重复判断,是质数筛选领域的经典高效算法。
五,为啥起始设置为i*i 步长为i
# 从i*i开始标记(优化),步长为i
for j in range(i * i, n + 1, i):
is_prime[j] = False
在埃式筛法中,从 i*i
开始标记 i
的倍数(而不是从 2*i
开始)是一个重要的优化手段 ,目的是避免重复标记已经被筛选过的合数,从而提高算法效率。
核心原因:i*2, i*3, ..., i*(i-1)
早已被标记过
对于质数 i
,它的倍数可以表示为 k*i
(k=2,3,4,...
)。但当 k < i
时,k*i
已经被比 i
小的质数标记过了,无需重复处理。
举个具体例子:当 i=5
(质数)时:
k=2
→5*2=10
:10 是 2 的倍数,早已在i=2
时被标记为合数;k=3
→5*3=15
:15 是 3 的倍数,早已在i=3
时被标记为合数;k=4
→5*4=20
:20 是 2 的倍数,早已在i=2
时被标记为合数;k=5
→5*5=25
:25 无法被比 5 小的质数(2,3)整除,此时才是第一次需要被标记的倍数。
原理推导:为什么 k < i
时 k*i
已被标记?
对于 k < i
:
- 若
k
是质数(如k=2,3
),则k*i
是k
的倍数,会在i
之前(当遍历到k
时)被标记; - 若
k
是合数(如k=4=2*2
),则k
一定包含小于k
的质因数(如 2),因此k*i
也是这个质因数的倍数,会在遍历到该质因数时被标记。
因此,所有 k < i
的 k*i
都已经被比 i
小的质数处理过了 ,只有 k ≥ i
时的 k*i
(即从 i*i
开始)才是未被标记的新合数,需要此时标记。
对比:从 2*i
开始 vs 从 i*i
开始
- 从
2*i
开始:正确,但会重复标记已处理的合数(如上述例子中的 10,15,20),增加无效操作; - 从
i*i
开始:同样正确,且避免了所有重复标记,减少了循环次数,提高效率(尤其是n
很大时,优化效果明显)。
特殊情况:当 i*i
超过 n
时
如果 i*i > n
(例如 n=10
,i=4
时 4*4=16 > 10
),此时 range(i*i, n+1, i)
会直接跳过(因为起点大于终点),不会执行标记操作,这也符合逻辑 ------ 此时 i
的所有有效倍数(≤n)都已被处理过了。
总结
从 i*i
开始标记 i
的倍数,是埃式筛法中利用 "小质数已优先处理其倍数" 的特性进行的优化,既保证了筛选结果的正确性,又减少了不必要的重复操作,使算法效率更高。
编辑分享
在埃式筛法中,标记质数 i
的倍数时,步长设置为 i
是为了精准地遍历 i
的所有倍数,既不会遗漏,也不会多余遍历无关数字,这是由 "倍数" 的数学性质决定的。
核心逻辑:i
的倍数之间的间隔恰好是 i
对于任意质数 i
,它的倍数可以表示为:i×2, i×3, i×4, ..., i×k
(其中 k
是正整数,且 i×k ≤ n
)。这些倍数之间的差值为 i
(例如 i×3 - i×2 = i
,i×4 - i×3 = i
),因此每间隔 i
个数字,就会出现一个 i
的倍数。
以 i=3
为例(需要标记 3 的所有倍数):
- 3 的倍数依次是:6, 9, 12, 15, ...(从
3×2
开始,或优化后从3×3=9
开始) - 这些数字之间的间隔都是 3(9-6=3,12-9=3,...),因此步长设为 3 时,就能依次遍历所有 3 的倍数。
为什么不能用其他步长?
- 若步长小于
i
(如步长 = 1):会遍历大量非i
倍数的数字(比如i=3
时,步长 = 1 会遍历 9,10,11,12,...,其中 10、11 不是 3 的倍数),导致无效操作,降低效率。 - 若步长大于
i
(如步长 =i+1
):会跳过部分i
的倍数(比如i=3
步长 = 4 时,从 9 开始会遍历 9,13,17,...,漏掉了 12,15 等 3 的倍数),导致筛选不完整,结果错误。
结合代码看效果
在 for j in range(i * i, n + 1, i)
中:
- 起点
i*i
:第一个需要标记的i
的倍数(优化后); - 终点
n+1
:确保能标记到 ≤n 的最后一个i
的倍数; - 步长
i
:每次跳过i
个数字,正好落在下一个i
的倍数上(i*i + i = i*(i+1)
,i*(i+1) + i = i*(i+2)
,...)。
例如 i=5
,n=30
时:
- 起点是
5×5=25
,步长 = 5,遍历的数字是 25、30(都是 5 的倍数),准确标记了所有需要处理的合数。
总结
步长设置为 i
,是由 "i
的倍数之间间隔为 i
" 这一数学性质决定的。它能确保:
- 正确性 :不遗漏任何
i
的倍数; - 高效性:不遍历无关数字,减少无效操作。
这是埃式筛法中保证筛选逻辑准确且高效的关键设计。
用埃式筛选法找质因数
质因数分解
模板一,预处理每个数的所有不同质因子。原理同埃氏筛。
Python3
Java
C++
Go
MX = 1_000_001
prime_factors = [[] for _ in range(MX)]
for i in range(2, MX):
if not prime_factors[i]: # i 是质数
for j in range(i, MX, i): # i 的倍数 j 有质因子 i
prime_factors[j].append(i)
作者:灵茶山艾府
链接:https://leetcode.cn/discuss/post/IYT3ss/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这个代码模板利用埃氏筛法的思想 ,高效预处理出一定范围内(这里是 1
到 1_000_000
)每个数的所有不同质因子 ,核心原理是:质数的倍数一定包含该质数作为质因子。
代码逻辑拆解
我们以具体步骤和例子说明其工作原理:
1. 初始化
- 定义范围
MX = 1_000_001
(即处理0
到1_000_000
的数)。 - 创建列表
prime_factors
,其中prime_factors[j]
用于存储j
的所有不同质因子(初始时每个元素都是空列表)。
2. 核心筛选过程
遍历 i
从 2
到 MX-1
:
-
判断
i
是否为质数 :若prime_factors[i]
是空列表,说明i
是质数。(原因:如果i
是合数,它一定有比它小的质因子,这些质因子在之前的遍历中已被添加到prime_factors[i]
中,所以列表不会为空。) -
标记
i
的所有倍数的质因子 :若i
是质数,则遍历i
的所有倍数j
(从i
开始,步长为i
),将i
添加到prime_factors[j]
中。(原因:j
是i
的倍数(j = k*i
),因此i
是j
的一个质因子,且每个质因子只会被添加一次,保证了列表中是 "不同的质因子"。)
举例说明
以 i=2
、i=3
、i=4
为例,看过程如何执行:
-
当
i=2
时 :prime_factors[2]
是空列表 →2
是质数。遍历j=2,4,6,8,...
(所有 2 的倍数),将2
加入prime_factors[j]
。此时:prime_factors[2] = [2]
,prime_factors[4] = [2]
,prime_factors[6] = [2]
,prime_factors[8] = [2]
,... -
当
i=3
时 :prime_factors[3]
是空列表 →3
是质数。遍历j=3,6,9,12,...
(所有 3 的倍数),将3
加入prime_factors[j]
。此时:prime_factors[3] = [3]
,prime_factors[6] = [2,3]
(之前已有 2),prime_factors[9] = [3]
,... -
当
i=4
时 :prime_factors[4] = [2]
(非空)→4
是合数(不是质数),跳过处理。
最终效果
预处理完成后,prime_factors[j]
直接给出 j
的所有不同质因子。例如:
prime_factors[12] = [2,3]
(12 的质因子是 2 和 3)prime_factors[30] = [2,3,5]
(30 的质因子是 2、3、5)prime_factors[7] = [7]
(7 是质数,质因子只有自身)
优势与适用场景
- 高效性 :预处理时间复杂度为
O(MX log log MX)
(与埃氏筛相同),后续查询任意数的质因子可在O(1)
时间完成。 - 空间换时间 :适合需要多次查询质因子的场景(如数论问题、分解质因数频繁的算法题)。
这个模板通过 "质数标记倍数" 的思路,巧妙复用了埃氏筛的思想,是处理质因数分解问题的高效工具