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)时间完成。 - 空间换时间 :适合需要多次查询质因子的场景(如数论问题、分解质因数频繁的算法题)。
这个模板通过 "质数标记倍数" 的思路,巧妙复用了埃氏筛的思想,是处理质因数分解问题的高效工具