1. 素数基础概念与常规判断方法
素数(质数)是指大于1的自然数中,除了1和它本身外不再有其他因数的数。理解素数的基本性质是掌握筛法的基础。我们先看一个简单的素数判断函数:
def is_prime(n): if n <= 1: return False for i in range(2, int(n**0.5)+1): if n % i == 0: return False return True这个函数的时间复杂度是O(√n),对于单个数的判断尚可接受,但当我们需要找出一定范围内的所有素数时,这种方法的效率就显得不足了。例如要找出1到10^6之间的所有素数,这种暴力方法需要进行约10^9次运算。
2. 埃拉托斯特尼筛法原理与实现
埃拉托斯特尼筛法(简称埃氏筛)是最古老的素数筛选算法,其核心思想是:
从2开始,将每个素数的倍数都标记为合数,剩下的就是素数
具体实现步骤:
- 初始化一个布尔数组is_prime[0..n],全部设为True
- 从2开始遍历到√n:
- 如果is_prime[i]为True,则i是素数
- 将i的所有倍数(从i*i开始)标记为False
- 最后所有is_prime[i]为True的i就是素数
Python实现:
def eratosthenes(n): is_prime = [True] * (n+1) is_prime[0] = is_prime[1] = False for i in range(2, int(n**0.5)+1): if is_prime[i]: for j in range(i*i, n+1, i): is_prime[j] = False return [i for i, prime in enumerate(is_prime) if prime]埃氏筛的时间复杂度是O(n log log n),空间复杂度O(n)。虽然比暴力方法高效,但仍存在重复标记的问题——例如数字6会被2和3都标记一次。
3. 线性筛法的核心思想与优势
线性筛法(欧拉筛)通过确保每个合数只被其最小质因数标记一次,将时间复杂度优化到真正的O(n)。其核心原理是:
- 维护一个质数列表primes和标记数组is_prime
- 对于每个数i,无论它是否是素数,都用已知的质数primes[j]去标记i*primes[j]为合数
- 当i能被primes[j]整除时立即终止内层循环
这种方法的精妙之处在于:
- 每个合数n只会被其最小质因数p标记(当i=n/p时)
- 每个数只被标记一次,避免了重复计算
- 同时还能记录每个数的最小质因数
4. 线性筛法的完整实现与解析
下面是线性筛法的Python实现:
def linear_sieve(n): is_prime = [True] * (n+1) primes = [] for i in range(2, n+1): if is_prime[i]: primes.append(i) for p in primes: if i * p > n: break is_prime[i * p] = False if i % p == 0: # 关键点:保证只被最小质因数标记 break return primes关键点解析:
if i % p == 0这一判断确保了每个合数只被其最小质因数标记- 例如当i=4时,只会标记4×2=8,而不会标记4×3=12(因为12会被i=6时用2标记)
- 内层循环的条件
i * p <= n控制了标记范围
时间复杂度分析:
- 外层循环执行n次
- 内层循环中,每个合数只被标记一次
- 因此总操作次数是O(n)
5. 线性筛法的应用与性能对比
线性筛法不仅高效,还能衍生出一些有用的副产品:
- 最小质因数表:可以记录每个数的最小质因数
def linear_sieve_with_spf(n): spf = [0] * (n+1) # smallest prime factor primes = [] for i in range(2, n+1): if spf[i] == 0: # i是素数 spf[i] = i primes.append(i) for p in primes: if p > spf[i] or i * p > n: break spf[i * p] = p return primes, spf性能对比(n=10^6):
- 暴力方法:约2秒
- 埃氏筛:约0.3秒
- 线性筛:约0.15秒
实际应用场景:
- 需要频繁查询素数或质因数分解
- 密码学中的素数生成
- 数论问题求解
6. 线性筛法的优化技巧与边界处理
在实际应用中,我们可以对线性筛法进行一些优化:
- 内存优化:使用位图代替布尔数组
import array def linear_sieve_bit(n): is_prime = array.array('B', [1]) * ((n+7)//8) primes = [] def get_bit(x): return (is_prime[x//8] >> (x%8)) & 1 def set_bit(x): is_prime[x//8] &= ~(1 << (x%8)) for i in range(2, n+1): if get_bit(i): primes.append(i) for p in primes: if i * p > n: break set_bit(i * p) if i % p == 0: break return primes分段筛法:处理超大范围的素数筛选
- 将区间分成适合内存的小块
- 用基础筛法预处理小素数
- 对每个块用预处理的小素数进行筛选
常见问题处理:
- 边界条件:n=0,1,2等特殊情况
- 大数处理:注意数值溢出问题
- 并行优化:多线程分段处理
7. 线性筛法在算法竞赛中的应用实例
例1:快速质因数分解
def factorize(x, spf): factors = {} while x > 1: p = spf[x] while x % p == 0: factors[p] = factors.get(p, 0) + 1 x = x // p return factors例2:欧拉函数计算
def euler_phi(n, spf): result = n while n > 1: p = spf[n] result = result // p * (p-1) while n % p == 0: n = n // p return result例3:素数计数函数π(n)
def prime_count(n): if n < 2: return 0 primes = linear_sieve(n) return len(primes)这些应用展示了线性筛法不仅是一个高效的素数生成工具,还是许多数论算法的基础构件。在算法竞赛中,预处理素数表往往是解决数论问题的第一步。