不多聊,开始。
一种朴素的想法,就是把每个数对它可能的因数取余,判断是否不存在能将其分解的数,并将其记录在表中。
public boolean[] makeCharts(int n) {
boolean[] charts = new boolean[n + 1];
for (int i = 2; i <= n; i++) {
boolean flag = false;
for (int k = 2; k * k <= i; k++)
if (flag = i % k == 0)
break;
charts[i] = !flag;
}
return charts;
}
这里解释一下k * k <= i
这个点。
如果存在大于 i \sqrt{i} i 的数 k 1 k_{1} k1 能作为 i i i 的因数,这里写作 i ÷ k 1 = k 2 i ÷ k_{1} = k_{2} i÷k1=k2,即 k 1 k 2 = i k_{1}k_{2} = i k1k2=i,我们将两遍同时除以一个 i \sqrt{i} i,得 k 1 i k 2 = i \cfrac{\ k_{1}}{\sqrt{i}}\ k_{2} = \sqrt{i} i k1 k2=i,由于 k 1 > i k_{1} > \sqrt{i} k1>i,所以 k 1 i > 1 \cfrac{\ k_{1}}{\sqrt{i}} > 1 i k1>1,故 k 2 < i k_{2} < \sqrt{i} k2<i,所以与 k 2 k_{2} k2 在顺序枚举时,会在 i \sqrt{i} i 之前出现,因此这种写法的正确性有了保障。
整个算法的时间复杂度为 O ( n 3 ) O(\sqrt{n^{3}}) O(n3)。
把这里做的改进单独列为一种的原因是,
我们打出一个质数表,场景通常是高频的判断,或者使用,使用布尔数组为区间内的质数打上标记固然使得判断速度大幅提升,但同时我们要使用一个范围内的质数时,似乎没有什么高效的方式可以实现。
在高频使用的场景,我们可以对朴素打表的方法做出一点小小的更改,把打上标记这一操作更变为将质数加入一个序列,但这里要给出的不是这种简单的变更,而是基于操作变更后的一定小改进。
public List<Integer> makeCharts(int n) {
List<Integer> charts = new ArrayList();
if (n >= 2) charts.add(2);
for (int i = 3; i <= n; i++) {
boolean flag = false;
for (int k : charts)
if (k * k > i || (flag = i % k == 0)) break;
if (!flag) charts.add(i);
}
return charts;
}
在朴素打表中,我们知道只需要枚举 i \sqrt{i} i 内的所有正整数,就能判断一个数是否为质数,而基本算术定理又能告诉我们,每个大于 1 1 1 的自然数都能分解成若干质数的幂的乘积,而在整个打表过程中,我们又能快速的遍历小于 i i i 的质数的集合,总和起来就是这个改进的全貌。
这里使用 n ln n \cfrac{n}{\ln n} lnnn 近似质数分布,该算法时间复杂度为 O ( n 3 log n ) O(\cfrac{\sqrt{n^{3}}}{\log \sqrt{n}}) O(lognn3)。
又称埃氏筛、爱氏筛、质数筛、普通筛法,是较为简单的质数筛法之一。
一个大于 1 1 1 的自然数要么是质数,要么是合数,如果我们将 n n n 以内的自然数集中,所有小于 n \sqrt{n} n 的质数的倍数全部筛选出来,剩下数的数集就是我们要的质数表。
public boolean[] makeCharts(int n) {
boolean[] charts = new boolean[n + 1];
for (int i = 2; i <= n; i++)
charts[i] = true;
for (int i = 2; i <= n; i++)
if (charts[i])
for (int k = i << 1; k <= n; k += i)
charts[k] = false;
return charts;
}
复杂度可以简单的考虑如下:
内层循环执行 n p − 1 \cfrac{n}{p} - 1 pn−1, p p p 为质数。
时间复杂度为 O ( ∑ p ≤ n n p − 1 ) O(\displaystyle\sum_{p\ \leq\sqrt{n}}\cfrac{n}{p} - 1) O(p ≤n∑pn−1),即 O ( n ∑ p ≤ n 1 p − 1 n ) O(n\displaystyle\sum_{p\ \leq\sqrt{n}}\cfrac{1}{p} - \cfrac{1}{n}) O(np ≤n∑p1−n1)。
为了方便计算,引入质数分布定理, π ( x ) \pi(x) π(x) 为小于 x x x 的质数个数, π ( x ) ≈ x ln x \pi(x)\approx\frac{x}{\ln x} π(x)≈lnxx,第 i i i 个质数约为 i ln i i \ln i ilni。
原 式 = O ( n I ) 原式=O(nI) 原式=O(nI), I = 1 2 + ∑ i = 2 n ln n ( 1 i ln i − 1 n ) I = \cfrac{1}{2} +\displaystyle\sum_{i = 2}^{\frac{n}{\ln n}}(\cfrac{1}{i \ln i} - \cfrac{1}{n}) I=21+i=2∑lnnn(ilni1−n1),这里把第一个质数拿了出来。
积分估计一下 I I I 的值, f ( x ) = 1 x ln x f(x) = \cfrac{1}{x \ln x} f(x)=xlnx1,求 ∫ 2 n ln n f ( i ) d i − ∫ 2 n ln n 1 n + 1 2 \displaystyle\int_{2}^{\frac{n}{\ln n}}f(i)\, di-\displaystyle\int_{2}^{\frac{n}{\ln n}}\cfrac{1}{n} + \cfrac{1}{2} ∫2lnnnf(i)di−∫2lnnnn1+21
上牛茨公式, F ( x ) = ln ln x F(x) = \ln \ln x F(x)=lnlnx, I = F ( n ln n ) − F ( 2 ) − ∫ 2 n ln n 1 n + 1 2 = ln ln n − ln ln ln n − ln ln 2 − n − 2 ln n n ln n + 1 2 I = F(\frac{n}{\ln n}) - F(2) -\displaystyle\int_{2}^{\frac{n}{\ln n}}\cfrac{1}{n} + \cfrac{1}{2} = \ln \ln n - \ln \ln \ln n - \ln \ln 2- \cfrac{n - 2 \ln n}{n \ln n}+ \cfrac{1}{2} I=F(lnnn)−F(2)−∫2lnnnn1+21=lnlnn−lnlnlnn−lnln2−nlnnn−2lnn+21
综上,埃氏筛时间复杂度为 O ( n log log n ) O(n\log \log n) O(nloglogn)。
同样是筛出合数,欧拉筛法的策略相较于埃氏筛有很大不同。
当遍历到任意数 n i n_{i} ni 时,我们将所有 p j n i p_jn_i pjni, p j ∈ p r i m e s p_j \in primes pj∈primes 筛掉,
首先要讨论的是这一步的完全性,对于将要访问的合数 n k n_{k} nk,我们可以将其表示为 p j m k p_{j}m_{k} pjmk,其中 p j p_{j} pj 为 n k n_{k} nk 的最小质因数,这是因为,对于一个合数,我们总是能表示成一个最小因数和另一个数的乘积,而最小因数总是一个质数。
因此 p j ≤ m k p_{j} \leq m_{k} pj≤mk,而我们在遍历到 m k m_{k} mk 时, p j p_{j} pj 已经加入了 p r i m e s primes primes 集合,故这种策略是完全的。
其次要讨论的是在 n i m o d p j = 0 n_i\mod \ \ p_j = 0 nimod pj=0 时不再继续筛选的正确性,当 n i n_i ni 是 p j p_j pj 的整数倍时,我们记 m i m_i mi 为 m i = n i ÷ p j m_i = n_i ÷ p_j mi=ni÷pj,对于新的合数 n k n_k nk 我们可以记为 n k = n i × p j + 1 = m i × p j × p j + 1 n_k = n_i × p_{j+1}=m_i × p_j × p_{j+1} nk=ni×pj+1=mi×pj×pj+1,这说明 n i × p j + 1 n_i × p_{j+1} ni×pj+1 是 p j p_j pj 的整数倍,现在不继续筛选,遍历到 n k n_k nk 前也会被 m i × p j + 1 m_i × p_{j+1} mi×pj+1 继续筛选。
public List<Integer> makeCharts(int n) {
List<Integer> charts = new ArrayList();
boolean[] marked = new boolean[n + 1];
for (int i = 2; i <= n; i++) {
if (!marked[i]) charts.add(i);
for (int p : charts) {
if (i * p > n) break;
marked[i * p] = true;
if (i % p == 0)break;
}
}
return charts;
}
每个合数只被筛选一次,故算法时间复杂度为 O ( n ) O(n) O(n)。
也因此,欧拉筛也被称为线性筛。