质数与约数
质数
定义
质数与合数的定义:质数与合数是针对所有大于
在大于
线性判断质数
很容易可以想到,只需要枚举
bool is_prime(int n) {
if (n < 2) return 0;
for (int i=2; i<=n-1; i++) {
if (n%i == 0) return 0;
}
return 1;
}
容易发现,上面这个代码的时间复杂度是
判断质数
在学习这个方法前,我们需要知道一个性质:如果
所以根据这个性质,我们只需要枚举完所有较小的数字即可,可以列出如下公式:
所以上面代码可以优化为:
bool is_prime(int n) {
if (n < 2) return 0;
for (int i=2; i<=n/i; i++) { // 写 i<=sqrt(n)也可以,但是还需要算,比较慢。
if (n%i == 0) return 0;
}
return 1;
}
这样代码的时间复杂度就变成了
分解质因数
分解质因数应该是很多同学都熟悉的数学题,要把一个合数分解成多个质数相乘的结果。容易想到一个解法,枚举所有可能的质数,然后让这个质数一直去除
这样做确实可以,但其实我们没有必要去枚举质数,直接枚举
假设当前枚举的数
那么可以得到如下代码:
void divide(int n) {
// 对数字 n 进行分解质因数
for (int i=2; i<=n-1; i++) {
if (n%i == 0) {
int cnt = 0;
while (n%i == 0) {
n /= i;
cnt++;
}
printf("%d %d\n", i, cnt);
}
}
}
同样的,这个代码也是
所以,我们枚举的范围同样可以只到
void divide(int n) {
// 对数字 n 进行分解质因数
for (int i=2; i<=n/i; i++) {
if (n%i == 0) {
int cnt = 0;
while (n%i == 0) {
n /= i;
cnt++;
}
printf("%d %d\n", i, cnt);
}
}
if (n > 1) printf("%d %d\n", n, 1);
}
质数筛
在需要多次判断质数的场景,比如统计某个区间内的所有质数时,
朴素筛
筛法的思想很简单,数字的倍数一定不是质数。那么在选到一个数字时,把这个数字的所有倍数全部筛掉,这样过一轮之后,所有的质数也就被筛选出来了。
如果要用STL实现,就把 primes
数组替换为 vector<int>
const int N = 1e6+5;
int primes[N], cnt; // primes用来存储质数,cnt是对应数组的下标
bool st[N]; // st[i]标记数字是否被筛掉
void get_primes(int n) {
// 筛选 2~n中的所有质数
for (int i=2; i<=n; i++) {
if (!st[i]) {
primes[++cnt] = i; // 质数存到数组中
}
for (int j=i+i; j<=n; j+=i) st[j] = 1; // 筛掉数字的倍数
}
// 这样过完之后,所有的质数就存到了primes数组中
// 同时 st 中标记了每个数字是否是质数。
}
这段代码的时间复杂度约为
埃氏筛
可以发现,其实没必要把所有的数字的倍数都遍历一遍,因为如果数字是一个合数,它的倍数肯定也被之前的质数筛掉了,所以只需要遍历质数的倍数;这也是筛法的核心思想:质数的倍数一定不是质数。
const int N = 1e6+5;
int primes[N], cnt; // primes用来存储质数,cnt是对应数组的下标
bool st[N]; // st[i]标记数字是否被筛掉
void get_primes(int n) {
// 筛选 2~n中的所有质数
for (int i=2; i<=n; i++) {
if (!st[i]) {
primes[++cnt] = i; // 质数存到数组中
for (int j=i+i; j<=n; j+=i) st[j] = 1; // 筛掉质数的倍数
}
}
// 这样过完之后,所有的质数就存到了primes数组中
// 同时 st 中标记了每个数字是否是质数。
}
这段代码的时间复杂度约为
欧拉筛(线性筛)
在埃氏筛中,为什么代码会耗时呢,是因为在遍历质数的倍数时,会重复标记很多数字,例如数字
有什么办法可以让每个数字都只被筛一次呢?先来看下面这段代码:
void get_primes(int n) {
// 筛选 2~n中的所有质数
for (int i=2; i<=n; i++) {
if (!st[i]) primes[++cnt] = i; // 质数存到数组中
// primes[j] <= n/i 理解为 primes[j]*i <= n,即只筛n范围内的数字
for (int j=1; primes[j]<=n/i; j++) {
st[primes[j]*i] = 1; // 筛掉i的素数倍数
if (i%primes[j] == 0) break; // 线性复杂度的关键
}
}
}
这段代码乍一看很莫名其妙,感觉既不能筛掉所有的合数,也没法保证
这个算法称为欧拉筛法,其核心思想是每个合数都可以分解为一个最小质因数乘以另一个数的形式,即 合数 = 最小质因数 * 自然数,在上述代码中,st[primes[j]*i] = 1
做的工作就是把以 primes[j]
为最小质因数的数字给筛掉,来看一段表格:
i | primes | 筛掉的数字 |
---|---|---|
2 | 2 | 4 |
3 | 2,3 | 6,9 |
4 | 2,3 | 8 |
5 | 2,3,5 | 10,15,25 |
6 | 2,3,5 | 12 |
7 | 2,3,5,7 | 14,21,35,49 |
8 | 2,3,5,7 | 16 |
9 | 2,3,5,7 | 18,27 |
10 | 2,3,5,7 | 20 |
首先可以发现,每个数字都是被自己的最小质因子筛掉的,比如 |
这还要基于 if (i%primes[j]==0) break;
这段代码来证明,根据这个条件,我们代码中会有两种情况:
情况一:i % primes[j] == 0
,那么 primes[j]
一定是 i
的最小质因子,否则肯定已经 break
了;因此 primes[j]*i
的最小质因子也必然是 primes[j]
。
情况二:i % primes[j] != 0
,那么 primes[j]
一定小于 i
的最小质因子,否则在前面肯定已经 break
了;因此 primes[j]*i
的最小质因子必然是 primes[j]
。
因为每个数字都会被最小质因子筛掉,所以每个数字实际都只会被标记依次,那么时间复杂度就接近
可能同学们还会有一个疑问,就是这样写代码不会漏掉一些数字么?当然不会,请看下面:
假设现在要筛掉 合数
以上便是关于欧拉筛的模板与证明,欧拉筛的时间复杂度为
约数
定义
约数的概念很简单,如果
线性枚举约数
容易想到,枚举
const int N = 1e5+5;
int res[N], cnt;
void get_divisors(int n) {
for (int i=1; i<=n; i++) {
if (n%i == 0) res[++cnt] = i;
}
}
显然,这样做的时间复杂度是
枚举约数
还是质数时讲过的性质:如果
const int N = 1e5+5;
int res[N], cnt;
void get_divisors(int n) {
for (int i=1; i<=n/i; i++) {
if (n%i == 0) {
res[++cnt] = i;
if (i != n/i) res[++cnt] = n/i; // 这个判断是防止加两次 n/i 进去
}
}
sort(res+1, res+1+cnt); // 如果不要求排序,也可以不用。
// 另外,也可以用两个数组,一个存前半部分一个存后半部分,最后合并,比排序更快。
}
例题-约数个数
一个数的约数个数可以通过其质因数分解来确定。具体来说,如果一个数
例如,数字
容易发现,
来看例题:
题目描述
给定
输入格式
第一行包含整数
接下来
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对
样例输入
3
2
6
8
样例输出
12
题目分析
根据上述公式可得,我们只需要对每个 unordered_map
来记录每个质因子的出现次数。
示例代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9+7;
unordered_map<int, int> mp;
void get_cnt(int n) {
for (int i=2; i<=n/i; i++) { // 分解质因数
while (n%i == 0) {
n /= i;
mp[i]++; // 统计这个质因数的指数
}
}
if (n > 1) mp[n]++; // 单独处理比 n/i 大的质因子
}
int main() {
int n, x;
long long res = 1;
cin >> n;
for (int i=1; i<=n; i++) {
cin >> x;
get_cnt(x);
}
for (auto i:mp) res = res*(i.second+1) % MOD; // 计算结果
cout << res;
return 0;
}
约数之和
题目描述
给定
输入格式
第一行包含整数
接下来
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对
样例输入
3
2
6
8
样例输出
252
题目分析
还是基于分解质因数的公式来计算。如果
我们来用实际的数字进行推导,
那
假设
即:
聪明的你再往下推一步,如果是
揭晓答案,应该是:
所以,根据数学归纳法可得,当
根据上面的公式,来求出这个代码吧!
如何计算
...
示例代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9+7;
unordered_map<int, int> mp;
void get_cnt(int n) {
// 此处实现同上段代码,不再演示
}
int main() {
int n, x;
long long res = 1;
cin >> n;
for (int i=1; i<=n; i++) { // 求出所有质因子的指数
cin >> x;
get_cnt(x);
}
for (auto i:mp) {
long long sum=1, a=i.first, b=i.second;
while (b--) { // 求解的关键,写法很多,也可以再来个变量,一项一项算然后求和。
sum = (sum*a+1) % MOD;
}
res = res*sum % MOD; // 一定要记得边算边模,否则数据会溢出
}
cout << res;
return 0;
}
最大公约数
定义
最大公约数(GCD),指两个或多个整数共有的约数中最大的一个,例如
线性找最大公约数
很容易想到,如果要求
int gcd(int a, int b) {
// 为什么是 min(a,b) 呢?因为约数的范围不会超过这个数字。
for (int i=min(a,b); i>=1; i--) {
if (a%i==0 and b%i==0) return i;
}
}
显然,这个代码的时间复杂度是
辗转相除法
欧几里得算法又称辗转相除法,原理为
令
设
因为
辗转相除法便是利用这个原理不断往下计算,直到
示例代码
int gcd(int a, int b) {
// 可以简写为 return b ? gcd(b, a%b) : a;
if (a%b == 0) return b;
else return gcd(b, a%b);
}