筛法一:朴素筛法(埃拉托斯特尼筛法/埃氏筛法)
一、原理
从 2 开始,用每一个质数去筛掉它的倍数。例如,要筛出 1 ~ 11 中的所有质数,则初始时:
1 2 3 4 5 6 7 8 9 10 11
注意 1 既不是质数也不是合数,所以直接去掉,然后从第一个质数 2 开始,将它的所有倍数筛掉:
初始数组: 1 2 3 4 5 6 7 8 9 10 11
被筛掉的数: 4 6 8 10
剩余的数: 2 3 5 7 9 11
然后,将下一个质数(3)的所有倍数筛掉:
初始数组: 2 3 4 5 6 7 8 9 10 11
被筛掉的数: 6 9
剩余的数: 2 3 4 5 7 8 10 11
再依次将下一个质数5、再下一个质数7、11的所有倍数筛掉,最终,没有被筛过的数即为 1~n 中所有的质数。
二、代码实现
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
bool st[N]; // 记录数字i是不是质数,true=是,false=不是
int getPrime(int n)
{
int cnt = 0; // 质数总数
for (int i = 2; i <= n; i++)
{
if (st[i]) continue;
else
{
cnt++;
// 用当前质数去筛掉它的倍数
for (int j = 2; j <= n / i; j++) // 从当前质数的2倍开始,一直到floor(n/i)倍(下取整:比自己小的最大整数;上取整:比自己大的最小整数)
st[j * i] = true;
}
}
return cnt;
}
int main()
{
int n;
cin >> n;
int res = getPrime(n);
cout << res << endl;
return 0;
}
三、时间复杂度分析
循环共执行
n
ln
n
\frac n {\ln n}
lnnn 轮(见下方质数定理),每一轮的循环长度依次是 n/2、n/3、n/5、n/7…,故一种粗略的估计方法为:
n
2
+
n
3
+
n
5
+
n
7
+
⋯
≈
n
ln
n
⋅
ln
n
=
O
(
n
)
\frac n 2 + \frac n 3 + \frac n 5 + \frac n 7 + \cdots \approx \frac n {\ln n} \cdot \ln n = O(n)
2n+3n+5n+7n+⋯≈lnnn⋅lnn=O(n)
但以上时间复杂度并非真实的时间复杂度,它只是一个粗略估计,真实的时间复杂度为
O
(
n
log
log
n
)
O(n \log \log n)
O(nloglogn)。
注:
(1)调和级数:
1
2
+
1
3
+
1
4
+
⋯
+
1
n
=
n
ln
n
\frac 1 2 + \frac 1 3 + \frac 1 4 + \cdots + \frac 1 n = n \ln n
21+31+41+⋯+n1=nlnn
(2)质数定理:1 ~ n 中有
n
ln
n
\frac n {\ln n}
lnnn 个质数。
筛法二:线性筛法(欧拉筛)
一、原理
- 每个合数只会被它的最小质因子筛掉;
- 每个合数仅会被筛一次;
- 时间复杂度为 O ( n ) O(n) O(n)。
以筛 1 ~ 20 之间的质数为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
i = 2, n = 20, primes[j] <= 10:
primes = [2] ---> st[4] = true, break;
i = 3, n = 20, primes[j] <= 6:
primes = [2, 3] ---> st[6, 9] = true, break;
i = 4, n = 20, primes[j] <= 5:
primes = [2, 3] ---> st[8] = true, break;
i = 5, n = 20, primes[j] <= 4;
primes = [2, 3, 5] ---> st[10, 15] = true, break;
二、代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
bool st[N];
int primes[N];
int getPrime(int n)
{
int cnt = 0;
for (int i = 2; i <= n; i++)
{
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; j++) // 欧拉筛是从前往后依次筛掉每个质数的倍数
{
st[primes[j] * i] = true; // i可以认为是当前每个质数的多少倍
if (i % primes[j] == 0) break; // 保证每个数只会被它的最小质因子筛掉
}
}
return cnt;
}
int main()
{
int n;
cin >> n;
int res = getPrime(n);
cout << res << endl;
return 0;
}
三、关键代码分析
(1)st[primes[j] * i] = true
当此行代码发生时,primes[j]
一定是 primes[j] * i
的最小质因子,理由如下:
- 当
i % primes[j] == 0
时,由于j
是从小到大遍历的,因此当本行代码发生时,primes[j]
一定是i
的最小质因子,因此也一定是primes[j] * i
的最小质因子; - 当
i % primes[j] != 0
时,primes[j]
一定小于i
的所有质因子,因此primes[j]
一定是primes[j] * i
的最小质因子。
综上,st[primes[j] * i] = true
和 if (i % primes[j] == 0) break;
这两行代码的联合使用,就保证了任何一个合数一定是被它的最小质因子 primes[j]
筛掉的。
四、时间复杂度分析
由于该算法保证每个合数只会被它的最小质因子筛掉,而每个合数的最小质因子是唯一的,因此每个数只会被筛一次,故算法的时间复杂度为 O ( n ) O(n) O(n)。
试除法求约数
一、算法原理
首先有如下事实:
若 d ∣ n d \mid n d∣n,则 n d ∣ n \frac n d \mid n dn∣n。(即若 d d d 能整除 n n n,则 n d \frac n d dn 也能整除 n n n。)
因此求 1 ∼ n 1 \sim n 1∼n 的所有约数时,不必从 1 枚举到 n,只需枚举到 n \sqrt n n 即可。
二、代码实现
#include <iostream>
#include <vector> // 用vector保存最终结果
#include <algorithm> // sort()函数
using namespace std;
vector<int> get_divisors(int x)
{
vector<int> res;
for (int i = 1; i <= x / i; i++)
{
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
}
sort(res.begin(), res.end());
return res;
}
int main()
{
int n;
cin >> n;
while (n--)
{
int x;
scanf("%d", &x);
auto res = get_divisors(x);
for (auto t : res) printf("%d ", t);
puts("");
}
return 0;
}
三、时间复杂度
由于循环最多进行到 n \sqrt n n ,故算法的时间复杂度为 O ( n ) O(\sqrt n) O(n)。
求约数个数
一、算法原理
算术基本定理:任何一个大于 1 的自然数
N
N
N,如果
N
N
N 不为质数,都可以唯一分解成有限个质数的乘积:
N
=
P
1
α
1
P
2
α
2
⋯
P
n
α
n
N = P_1 ^{\alpha_1} P_2 ^{\alpha_2} \cdots P_n ^{\alpha_n} \notag
N=P1α1P2α2⋯Pnαn
这里
P
1
<
P
2
<
⋯
<
P
n
P_1 < P_2 < \cdots < P_n
P1<P2<⋯<Pn 均为质数,其诸指数
α
i
\alpha_i
αi 是正整数。这样的分解称为
N
N
N 的标准分解式。
故自然数
N
N
N 的所有约数的个数为:
(
α
1
+
1
)
(
α
2
+
1
)
+
⋯
+
(
α
n
+
1
)
(\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1)
(α1+1)(α2+1)+⋯+(αn+1)
推导:
N
N
N 的任何一个约数都可以写成
d
=
P
1
β
1
P
2
β
2
…
P
n
β
n
d = P_1 ^ {\beta _1} P_2 ^ {\beta _2}\ldots P_n ^ {\beta _n}
d=P1β1P2β2…Pnβn
的形式。其中,
β
1
\beta _1
β1 可以取遍
0
∼
α
1
0 \sim \alpha_1
0∼α1 之间的所有整数,
β
2
\beta _2
β2 可以取遍
0
∼
α
2
0 \sim \alpha_2
0∼α2 之间的所有整数,…,
β
n
\beta_n
βn 可以取遍
0
∼
α
n
0 \sim \alpha_n
0∼αn 之间的所有整数,由分步乘法计数原理,所有可能的组合共有
(
α
1
+
1
)
(
α
2
+
1
)
+
⋯
+
(
α
n
+
1
)
(\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1)
(α1+1)(α2+1)+⋯+(αn+1) 种,每一种不同的组合就代表了一个不同的约数,因此所有的约数共有
(
α
1
+
1
)
(
α
2
+
1
)
+
⋯
+
(
α
n
+
1
)
(\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1)
(α1+1)(α2+1)+⋯+(αn+1) 个。
最大公约数(Greatest Common Divisor):