【算法】【数学】【质数】质数相关 leetcode 204

204. 计数质数 - 力扣(LeetCode)

204. 计数质数 - 力扣(LeetCode)

2521. 数组乘积中的不同质因数数目 - 力扣(LeetCode)

参考文档: 分享丨【算法题单】数学算法(数论/组合/概率期望/博弈/计算几何/随机算法) - 讨论 - 力扣(LeetCode)

埃式筛选法

埃拉托斯特尼筛法(简称 "埃式筛法")是一种高效查找一定范围内所有质数 的经典算法,由古希腊数学家埃拉托斯特尼提出。其核心思想是:通过排除已知质数的所有倍数,剩下的未被排除的数就是质数。

一、算法原理(步骤)

以 "找出小于等于 n 的所有质数" 为例,步骤如下:

  1. 初始化标记数组 :创建一个长度为 n+1 的布尔数组 is_prime,其中 is_prime[i] 表示 "数字 i 是否为质数"。

    • 初始时假设所有数字都是质数,即 is_prime[i] = True01 除外,它们不是质数,直接设为 False)。
  2. 筛选过程 :从第一个质数 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 开始标记)。
  3. 终止条件 :遍历到 √n 即可停止。因为大于 √n 的合数,其因子一定小于 √n,早已被标记过。

  4. 结果提取 :最终 is_prime 中值为 True 的索引,就是小于等于 n 的所有质数。

二、示例:筛选 30 以内的质数

  1. 初始化 is_prime[0..30] = [False, False, True, True, ..., True](0 和 1 为 False,其余为 True)。
  2. i=2 开始:
    • i=2 是质数,标记 4,6,8,...,30False
  3. 下一个 i=3(仍为 True):
    • 标记 9,12,15,...,30False(6、12 等已被 2 标记,不影响)。
  4. 下一个 i=4(已被标记为 False,跳过)。
  5. 下一个 i=5(仍为 True):
    • 标记 25, 30False(10、15 等已被标记)。
  6. 继续直到 i=√30≈5.47,遍历到 i=5 即可停止。
  7. 剩余 is_prime[i]=Truei 为: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 的数组存储标记。
  • 适用场景 :需要一次性找出 "较小范围内所有质数" 的场景(如 n10^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*ik=2,3,4,...)。但当 k < i 时,k*i 已经被i 小的质数标记过了,无需重复处理。

举个具体例子:当 i=5(质数)时:

  • k=25*2=10:10 是 2 的倍数,早已在 i=2 时被标记为合数;
  • k=35*3=15:15 是 3 的倍数,早已在 i=3 时被标记为合数;
  • k=45*4=20:20 是 2 的倍数,早已在 i=2 时被标记为合数;
  • k=55*5=25:25 无法被比 5 小的质数(2,3)整除,此时才是第一次需要被标记的倍数。

原理推导:为什么 k < ik*i 已被标记?

对于 k < i

  • k 是质数(如 k=2,3),则 k*ik 的倍数,会在 i 之前(当遍历到 k 时)被标记;
  • k 是合数(如 k=4=2*2),则 k 一定包含小于 k 的质因数(如 2),因此 k*i 也是这个质因数的倍数,会在遍历到该质因数时被标记。

因此,所有 k < ik*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=10i=44*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 = ii×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=5n=30 时:

  • 起点是 5×5=25,步长 = 5,遍历的数字是 25、30(都是 5 的倍数),准确标记了所有需要处理的合数。

总结

步长设置为 i,是由 "i 的倍数之间间隔为 i" 这一数学性质决定的。它能确保:

  1. 正确性 :不遗漏任何 i 的倍数;
  2. 高效性:不遍历无关数字,减少无效操作。

这是埃式筛法中保证筛选逻辑准确且高效的关键设计。

用埃式筛选法找质因数

复制代码
质因数分解
模板一,预处理每个数的所有不同质因子。原理同埃氏筛。

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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个代码模板利用埃氏筛法的思想 ,高效预处理出一定范围内(这里是 11_000_000)每个数的所有不同质因子 ,核心原理是:质数的倍数一定包含该质数作为质因子

代码逻辑拆解

我们以具体步骤和例子说明其工作原理:

1. 初始化
  • 定义范围 MX = 1_000_001(即处理 01_000_000 的数)。
  • 创建列表 prime_factors,其中 prime_factors[j] 用于存储 j 的所有不同质因子(初始时每个元素都是空列表)。
2. 核心筛选过程

遍历 i2MX-1

  • 判断 i 是否为质数 :若 prime_factors[i] 是空列表,说明 i 是质数。(原因:如果 i 是合数,它一定有比它小的质因子,这些质因子在之前的遍历中已被添加到 prime_factors[i] 中,所以列表不会为空。)

  • 标记 i 的所有倍数的质因子 :若 i 是质数,则遍历 i 的所有倍数 j(从 i 开始,步长为 i),将 i 添加到 prime_factors[j] 中。(原因:ji 的倍数(j = k*i),因此 ij 的一个质因子,且每个质因子只会被添加一次,保证了列表中是 "不同的质因子"。)

举例说明

i=2i=3i=4 为例,看过程如何执行:

  • i=2prime_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=3prime_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=4prime_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) 时间完成。
  • 空间换时间 :适合需要多次查询质因子的场景(如数论问题、分解质因数频繁的算法题)。

这个模板通过 "质数标记倍数" 的思路,巧妙复用了埃氏筛的思想,是处理质因数分解问题的高效工具

相关推荐
一碗白开水一3 小时前
【第29话:路径规划】自动驾驶启发式搜索算法(A星搜索算法( A* 搜索算法))详解及代码举例说明
人工智能·算法·机器学习·计算机视觉·自动驾驶·unix
小欣加油3 小时前
leetcode 98 验证二叉搜索树
c++·算法·leetcode·职场和发展
koping_wu3 小时前
【分布式】分布式ID生成方案、接口幂等、一致性哈希
分布式·算法·哈希算法
windliang3 小时前
一文入门 agent:从理论到代码实战
前端·算法
CoovallyAIHub3 小时前
华为发布开源超节点架构,以开放战略叩响AI算力生态变局
算法·架构·github
博笙困了4 小时前
AcWing学习——链表
c++·算法
NAGNIP4 小时前
AI训练要的数据这么多,怎么存?
算法
CoovallyAIHub4 小时前
冻结比微调更好?YOLOv8/V10迁移学习最佳实践发布,GPU内存直降28%
深度学习·算法·计算机视觉
爱编程的化学家4 小时前
代码随想录算法训练营第21天 -- 回溯4 || 491.非递减子序列 / 46.全排列 /47.全排列 II
数据结构·c++·算法·leetcode·回溯·全排列·代码随想录