质数与约数
质数
概念 : 在大于1的整数中 , 如果只包含1和本身这两个约数 , 就被称为质数 , 或者称作素数.
质数的判定 – 试除法 :
时间复杂度 : O ( sqrt(n) )
d | n --> n/d | n ( “|” 表示整除 )
只需判断较小的那个约数 , d^2 <= n ( d <= sqrt(n) )–> d <= n/d
注意 : 这里在循环中的退出条件一般写成 i <= n / i , 不推荐使用 sqrt函数 , 因为每次循环都需要调用一遍 sqrt函数 , 而sqsrt函数的速度较慢 , 导致整体速度较慢.
也不推荐使用 i * i <= n , 因为当数据接近于 int型的时候很有可能会溢出.
代码如下 :
bool is_prime(int x)
{
if(n < 2) return false ;
for(int i = 2;i < n / i;i ++ )
if(n % i == 0)
return false ;
return true ;
}
分解质因数 – 试除法 :
原理 : n 中最多只包含一个大于 sqrt(n) 的质因子
时间复杂度 : 最好情况 (n == 2^k): log ( n ) 最坏情况 : sqrt( n )
思路 : 从小到大枚举所有数
关于为什么不会筛到合数的原因 : 因为我们这里是从小到大进行枚举的 , 一旦可以整除 , 就一直除直到不能除为止 , 所以当枚举到第 i 个数的时候 , 其前面的 i - 1 个数的质因子已全被筛去 , 此时如果其能够被 x 整除 , 那么它必然就是 x 的一个质因子 . 原理同埃筛.
代码如下 :
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0) // i 一定是质数
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl; // 大于 sqrt(n) 的质因子
cout << endl;
}
线性筛法求素数
原理 : 将所有的数写到一个数表中 , 从小到大遍历 , 将每一个数的所有倍数删去 , 遍历完成后 , 所有剩下的数都为质数 .
假设对于任意一个数 p 而言 , 如果其没有被删去 , 则说明 2 ~ p-1 中没有一个数是它的因子 , 所以此时只有 1 和 它本身是它的因子 , 因此其为质数.
代码如下 :
朴素筛法(埃筛) – 优化后 :
其思想很重要 , 可以进行推广.
原理 : 当枚举到 p 时 , 只需要判断 p 之前的 2 ~ p-1 中的质数是不是p的因子即可 , 不需要所有数全部判断一遍 , 因为 , 在此之前其所有非质数的数会被全部删去 .
只需要删去所有质数的倍数的数 , 而不需要删去不是质数的倍数的数 ( 这样会造成重复删除 , 效率减慢 )
质数定理 : 在 1 ~ n 当中 , 有 ( n / log n ) 个质数
因此在枚举的时候 , 只需枚举 2 ~ log n , 也就是枚举 logn 个数.
时间复杂度 : O ( n ) (粗略估计情况) 真实情况 : O ( n*log(logn) )
#include <iostream>
#include <algorithm>
using namespace std ;
const int N = 1000010 ; int primes[N] , cnt ; // cnt 用来给素数表计数
bool st[N] ; // 用来判定一个数是否被删去
// 朴素筛法 :
void get_primes(int n)
{
for(int i = 2;i <= n;i ++ ) // 遍历 i ~ n 数表
{
if (!st[i]) // 如果该数没有被删去
{
primes[cnt ++ ] = i ; // 将其加入到素数表中
for(int j = i + i;j <= n;j += i) st[j] = true ; // 将所有质数i的倍数的数删去
}
}
}
int main()
{
get_primes(1000) ;
for(int i = 0;i < cnt;i ++ ) printf("%d ",primes[i]) ;
cout << endl ;
return 0 ;
}
线性筛法:
原理 : 争取将每一个合数用其质因子筛掉
可以将其与上述的 埃筛 进行对比
n 只会被它的最小质因子筛掉
i % primes[j] == 0
primes[j] 一定是 i 的最小质因子 , primes[j] 也一定是 primes[j] * i 的最小质因子
i % primes[j] != 0 , 因此 primes[j] 一定小于 i 的所有质因子 , 所以 primes[j] 也一定是 primes[j] * i 的最小质因子
综合以上两点 , 可得出该结论 : 每一个被筛掉的数都一定是被其最小质因子给筛掉的
对于一个合数 x , 其一定存在一个最小质因子 , 假设 primes[j] 是 x 的最小质因子 , 当 i 枚举到 x / primes[j] 的时候 , 都一定会被筛掉 , 在 i 枚举到 x 之前.
并且每次筛的时候都用其最小质因子来筛 , 又因为每个数只有一个最小质因子 , 因此每个数只会被筛一次 , 因此其是线性的.
时间复杂度 ; O ( n )
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
// 不需要加 j < cnt 这个条件,当 i 为质数的时候 , primes会在最小质因子的时候停下,当i为质数的时候,primes会在枚举到该轮的质数的时候停下来 , 即 primes[j]^2 <= n
for (int j = 0; primes[j] <= n / i; j ++ ) // 注意这里的跳出条件
{
st[primes[j] * i] = true; // 删去所有质数倍数的数字,且这个数会小于我们给定的 n ,
// 如果 i 是质数的话 , 就处理 , 否则不进行删除操作(重复)
// 因为在此算法中 , 我们仅仅删去质数倍数的数
if (i % primes[j] == 0) break; // primes[j]一定是i的最小质因子
}
}
}
约数
基本概念 : 约数,又称因数。整数a除以整数b(b≠0) 除得的商正好是整数而没有余数,我们就说a能被b整除,或b能整除a。a称为b的倍数,b称为a的约数。
试除法求一个数的所有约数 :
原理: d | n --> n/d | n 一个数的约数是成对出现的 , 我们在枚举的时候枚举较小的那个约数即可 , 枚举到满足 d <= n/d --> d <= sqrt(n)
从小到大枚举小的那个约数 , 注意判断边界情况,.
时间复杂度 : sqrt(n)
代码如下 :
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std ;
vector<int> get_divisors(int n)
{
vector<int> res ;
for(int i = 1;i <= n / i;i ++ )
{
if(n % i == 0)
{
res.push_back(i) ;
if(i != n / i) res.push_back(n / i) ; // 这里一定要判断边界情况 : i^2 == n
}
}
sort(res.begin() , res.end()) ; // 对约数数组进行从小到大排序
return res ;
}
int main()
{
int n ; cin >> n ;
while (n -- )
{
int x ;
cin >> x ;
auto res = get_divisors(x) ;
for (auto t : res) cout << t << " " ;
cout << endl ;
}
return 0 ;
}
约数个数 :
算术基本定理,又称为正整数的唯一分解定理,即:每个大于1的自然数,要么本身就是质数,要么可以写为2个或以上的质数的积,而且这些质因子按大小排列之后,写法仅有一种方式。
例如: 6936 = 2^3 * 3 * 17^2 , 1200 = 2^4 * 3 * 5^2
算术基本定理的内容由两部分构成:
1.分解的存在性:
2.分解的唯一性,即若不考虑排列的顺序,正整数分解为素数乘积的方式是唯一的。
公 式 : N = P 1 x 1 ⋅ P 2 x 2 ⋅ . . . ⋅ P k x k 公式 : N = {P_1}^{x_1} \cdot {P_2}^{x_2} \cdot \ ... \ \cdot \ {P_k}^{x_k} 公式:N=P1x1⋅P2x2⋅ ... ⋅ Pkxk
约 数 个 数 公 式 : ( X 1 + 1 ) ⋅ ( X 2 + 1 ) ⋅ . . . ⋅ ( X k + 1 ) 约数个数公式 : \ (X_1 + 1) \ \cdot \ (X_2 + 1) \ \cdot \ ... \ \cdot(X_k + 1) 约数个数公式: (X1+1) ⋅ (X2+1) ⋅ ... ⋅(Xk+1)
公式解释 :
注意 : 此公式下所用到的其因子全为质因子 , 因为其他非质的因子可以通过质因子的倍数来累乘得到
N 的任意一个约数 d 也可以写成 d = P 1 B 1 P1^{B_1} P1B1 * P 2 B 2 {P_2}^{B_2} P2B2 * … * P k B k {P_k}^{B_k} PkBk 的形式,
每一个 Bi 都是在 0 ~ Xi 之间的 , 并且每一项 Bi 的指数不同的话 , 这个约数就不同.
因此 , 所有的约数个数就和 Bi 的取法的个数相同.
例 如 : 6 = 2 1 × 3 1 例如: 6 = 2^1 \times 3^1 例如:6=21×31
此时其约数个数为 (1 + 1) * (1 + 1) = 4 ;
因为每一位有 0 ~ n 种取法 , 所以为 n + 1
int 范围内 , 约数个数最多的数的约数为1650个左右
模版 : 给定 n 个正整数 a i a_i ai , 请输出这些数的乘积的约数的个数 , 答案对 1 0 9 + 7 10^9 + 7 109+7取模
输入样例 :
3
2
6
8
输出样例 :
12
代码如下 :
// 给定 n 个正整数 $a_i$ , 请输出这些数的乘积的约数的个数
// map key存底数 value存每个质因子的指数
#include <iostream>
#include <algorithm>
#include <unordered_map> // 使用hash表(unordered_map)需要包含这个头文件
using namespace std ;
const int mod = 1e9 + 7 ;
typedef long long LL ;
int main()
{
int n ; cin >> n ;
unordered_map<int,int> primes ;
while (n -- )
{
int x ; cin >> x ;
for(int i = 2;i <= x / i; i ++ )
{
if (x % i == 0)
{
while (x % i == 0)
{
x /= i ;
primes[i] ++ ;
}
}
}
if (x > 1) primes[x] ++ ;
}
LL res = 1 ;
for (auto prime : primes) res *= (prime.second + 1) % mod ;
cout << res << endl ;
return 0 ;
}
约数之和 :
公 式 : N = P 1 x 1 ⋅ P 2 x 2 ⋅ . . . ⋅ P k x k 公式: N = {P_1}^{x_1} \cdot {P_2}^{x_2} \cdot \ ... \ \cdot \ {P_k}^{x_k} 公式:N=P1x1⋅P2x2⋅ ... ⋅ Pkxk
约 数 之 和 公 式 : ( P 1 0 + P 1 1 + . . . P 1 X 1 ) ⋅ . . . ⋅ ( P k 0 + . . . + P k X k ) 约数之和公式 : \ ({P_1}^{0} + {P_1^1} + \ ... \ {P_1}^{X_1}) \ \cdot ... \ \cdot ({P_k^0} \ + \ ... \ + \ {P_k}^{X_k}) 约数之和公式: (P10+P11+ ... P1X1) ⋅... ⋅(Pk0 + ... + PkXk)
公式解释 : 将该式展开后可知其一定是一堆乘积的形式 , 这样的乘积有 约数个数 之个 , 等于从每个括号里面选一个数进行相乘 , 最终得到的每个乘积的形式均为 : N = P 1 x 1 ⋅ P 2 x 2 ⋅ . . . ⋅ P k x k N = {P_1}^{x_1} \cdot {P_2}^{x_2} \cdot \ ... \ \cdot \ {P_k}^{x_k} N=P1x1⋅P2x2⋅ ... ⋅ Pkxk ,因此我们可以知道每个乘积的结果都是一个约数 ,也就是每一项都是一个约数,每一项又各不相同, 这些数的总数又等于约数的个数,所以所有的乘积之和就是所有的约数之和 .
模版 : 给定 n 个正整数 a i a_i ai , 请输出这些数的乘积的约数之和 , 答案对 1 0 9 + 7 10^9 + 7 109+7取模
与上述求约数的代码大体一致,先分解质因子 , 只是最后一步公式不一样
输入描述 :
3
2
6
8
输出描述 :
252
约数之和公式可以化简为 : 1 + p 1 p^1 p1 + p 2 p^2 p2 + … + p n p^n pn = t*p + 1 ; 就是不断提一个因子出来到括号外面,不断提一直到不能够提为止
代码如下 :
// 给定 n 个正整数 $a_i$ , 请输出这些数的乘积的约数之和
// map key存底数 value存每个质因子的指数
#include <iostream>
#include <algorithm>
#include <unordered_map> // 使用hash表(unordered_map)需要包含这个头文件
using namespace std ;
const int mod = 1e9 + 7 ;
typedef long long LL ;
int main()
{
int n ; cin >> n ;
unordered_map<int,int> primes ;
while (n -- )
{
int x ; cin >> x ;
for(int i = 2;i <= x / i; i ++ )
{
if (x % i == 0)
{
while (x % i == 0)
{
x /= i ;
primes[i] ++ ;
}
}
}
if (x > 1) primes[x] ++ ;
}
LL res = 1 ;
for (auto prime : primes)
{
LL t = 1 ;
int p = prime.first , a = prime.second ; // p代表这个质数的底数,a代表这个质数的指数
while (a -- ) t = (t * p + 1) % mod ;
res = res * t % mod ;
}
cout << res << endl ;
return 0 ;
}
最大公约数 :
欧几里得算法 (辗转相除法) :
数论中的一些基本性质 : d | a , d | b --> d | a + b --> ax + by 符号 “|” 表示整除
核心原理 :
gcd(a,b) = gcd(b,a mod b) ---- a和b的最大公约数等于b和 a mod b 的最大公约数
a mod b == a - a/b(向下取整) * b = a - c * b
(a , b) = (b , a - c * b )
解释 : 对于任意一个公约数 d , d | a , d | b ; – > d | b , d || (a - b*c) ,因此(a , b) = (b , a - c * b )
因此左边的任何一个公约数都是右边的公约数
右边的任何一个公约数也是左边的公约数
所以这两个公约数的集合是相同的,因此左边的最大公约数等于左边的最大公约数
利用此性质计算最大公约数
模版 : 给定 n 对正整数 a i a_i ai , b i b_i bi , 请你求出每对的最大公约数
输入样例 :
2
3 6
4 6
输出样例 :
3
2
代码如下 :
#include <iostream>
using namespace std ;
int gcd(int a,int b)
{
return b ? gcd(b,a % b) : a ; // 当 b = 0 时, 此时即求 a 和 0 的最大公约数,即为 a
}
int main()
{
int n ; cin >> n ;
while (n -- )
{
int a , b ; cin >> a >> b ;
printf("%d \n",gcd(a,b)) ;
}
return 0 ;
}
核心代码 ;
int gcd(int a,int b)
{
return b ? gcd(b,a % b) : a ; // 当 b = 0 时, 此时即求 a 和 0 的最大公约数,即为 a
}