🔔由理论推导到代码实践逐步精准掌握数论 (一)
💓质数
🌟基本概念
如果一个正整数只能被1和自身整除,那么这个正整数就是质数(也称素数),否则称该正整数为合数 |
🌟质数的判定——试除法
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌻参考代码(C++版本)
#include <iostream>
#include <algorithm>
using namespace std;
int n;
bool is_prime(int n)
{
//0 和 1既不是质数,也不是合数
if(n < 2) return false;
for(int i = 2;i <= n / i ;i++)
if(n % i == 0) return false;
return true;
}
int main()
{
//输入
scanf("%d",&n);
while(n--)
{
//调用函数 + 输出
int t;
scanf("%d",&t);
if(is_prime(t)) puts("Yes");
else puts("No");
}
}
🌻算法模板
试除法判定质数
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
return false;
return true;
}
🌻疑难点剖析
一、算法实现思路
扫描2~
N
\sqrt{N}
N之间的所有整数,依次检查它们能否整除N,若都不能整除,那么N是质数,否则N是合数。所以时间复杂度是O(
N
\sqrt{N}
N)
二、算法实现的优化
这个算法从暴力的角度走,是可以直接从2枚举到N-1的,只要这里面都没有能够整除N的,那么就可以确定这个N是质数,但是这种做效率挺慢的,时间复杂度是O(N) |
比如现在N是17,先去整除4进行尝试,假如再尝试8, 12,16其实都没有意义了,因此就根据性质进行优化。 |
乘法运算中,乘数 x 另一个乘数 = 积。 当 n 能够整除 d 的时候,结果是 n/d,同时,它也就是另一个乘数,那么n也肯定是能够整除n/d的。 因为我们是从2开始枚举,d依次被赋值为2,3,,,所以d < = n/d。依据这个实现对枚举区间的缩小 |
🌟分解质因数
算术基本定理: 对于任何一个大于1的正整数都能唯一分解为有限个质数的乘积,可写作: |
N = P1c1 P2c2 …Pmcm
其中ci都是正整数,Pi都是质数,且满足P1 < P2 < … < Pm
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌻参考代码(C++版本)
#include <iostream>
#include <algorithm>
using namespace std;
void divide(int n)
{
//试除法枚举所有的数
for(int i = 2;i <= n / i;i++)
if(n % i == 0)
{
int s = 0;
while(n % i == 0)
{
n /= i;
s++;
}
printf("%d %d\n",i,s); //要清楚i才是底数
}
if(n > 1) printf("%d %d\n",n,1);
puts("");
}
int main()
{
//输入
int n;
scanf("%d",&n);
while(n--)
{
//调函数
int x;
scanf("%d",&x);
divide(x);
}
return 0;
}
🌻算法模板
一、算法实现流程图:
二、算法的代码实现:
试除法分解质因数
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
🌻疑难点剖析
一、明白题目需求
题目要的是将传入的数据分解为底数是/质数,指数是整数,分解结果的乘积等于原数据,输出的时候,要从小到大排列。 |
例如:
6就是21 x 31,因此就输出 2 1和3 1
48就是24 x 31,因此就输出2 4 和 3 1
二、指数的获取
8 = 23 = 2 x 2 x 2;
那么在代码层面,我们可以采用逆向思维,倒着对8进行运算,统计它进行的除法次数,也就是相乘的次数了。同时,也就是指数了。 |
🌟质数筛选
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
参考代码(C++版本)
// 埃氏筛选法
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int primes[N],cnt;
bool st[N];
void get_primes(int n)
{
for(int i = 2;i <= n;i++)
{
if(!st[i])
{
primes[cnt++] = n;
//利用现有数,将这些数的倍数去掉
for(int j = i+i; j <= n;j += i) st[j] = true;
}
}
}
int main()
{
//输入
int n;
cin >>n;
//调用函数
get_primes(n);
//输出
cout << cnt <<endl;
return 0;
}
//线性筛选法 算法原理:n只会被最小质因子筛掉
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int primes[N],cnt;
bool st[N];
void get_primes(int n)
{
for(int i = 2;i <= n;i++)
{
//如果是质数,就把加到数组中去
if(!st[i]) primes[cnt ++] = i;
//从小到大枚举所有的质数
for(int j = 0; primes[j] <= n/i;j++)
{
//把prims[j] * i筛掉
st[primes[j] * i] = true;
if(i % primes[j] == 0) break; //primes[j]一定是i的最小质因子
}
/*
1、i % pj == 0
pj一定是i的最小质因子,pj一定是pj*i的最小质因子
2、i % pj != 0
pj一定小于i的所有质因子,pj也一定是pj * i的最小质因子
*/
}
}
int main()
{
//输入
int n;
cin >>n;
//调用函数
get_primes(n);
//输出
cout << cnt <<endl;
return 0;
}
🌻算法模板
一、埃氏筛法——用当前已有的质数去消去它们的倍数
首先,将2到n范围的所有整数获取到。其中,最小的数字2是质数,将2的所有倍数划去。表中剩余的数字中,最小的是3,它不能被更小的整除,所有它是质数,再将所有3的倍数划去。以此类推如果表中剩余的最小数字是m时,m就是质数。然后将表中所有m的倍数都划去。像这种反复操作,就能够依次枚举n以内的质数。 |
举个栗子:
1.1、 算法实现流程如下:
1.2、算法代码实现:
埃氏筛法求素数
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (st[i]) continue;
primes[cnt ++ ] = i;
for (int j = i; j <= n; j += i)
st[j] = true;
}
}
1.3、算法的时间复杂度 = O(NloglogN)
二、线性筛法
线性筛选的核心是—— 传入的整数n只会被最小质因子筛掉 |
2.1、算法实现流程:
2.2、算法代码实现:
线性筛法求素数
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n)
{
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;
if (i % primes[j] == 0) break;
}
}
}
2.3、算法时间复杂度 = O(N)
🌻疑难点剖析
清楚primes数组和st数组是分别用来维护素数集合和被筛除数据的集合 |
💓约数
🌟基本概念
若整数 n 除以整数 d 的余数为零,即 d 能够整除 n,则称 d 是 n 的约数,n 是 d 的倍数,记作:d|n |
🌟约数的判定——试除法
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌻参考代码(C++版本)
#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);
}
//按照从小到大的顺序输出它的所有约数。
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;
}
🌻算法模板
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;
}
🌻疑难点剖析
一、优化
最暴力的做法依旧是可爱的枚举,从1枚举到x,但是和求质数算法一样,当3是约数的时候,就不必去枚举12,15等等 |
二、结果统计
if (x % i == 0)
{
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
利用的性质是:两数相乘,积 = 乘数 x 另一个乘数。 x % i == 0说明 i 是 x 的一个乘数,则另一乘数就是 x/i,将其加入集合 |
🌟统计约数的个数
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌻参考代码(C++版本)
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
int main()
{
//输入
int n;
cin >> n;
//用STL容器开一个哈希表存分解出来的数的底数和指数
unordered_map<int,int> primes;
while(n--)
{
int x;
cin >> x;
//分解质因数的算法模板来分解这个输入的x
for(int i = 2; i <= x / i;i++)
while(x % i == 0)
{
x /= i;
primes[i] ++;//使i这个质因数的指数加1
}
if(x > 1) primes[x] ++; //对传入的x直接就是质因子的情况进行处理
}
//输出
LL res = 1;
for(auto prime : primes) res = res * (prime.second + 1) % mod;
cout << res << endl;
return 0;
}
🌻算法模板
求约数个数算法实现流程:
🌟统计约数的和
🌻例题描述
🌻参考代码(C++版本)
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
int main()
{
//输入
int n;
cin >> n;
//开一个哈希表存分解出来的数的底数和指数
unordered_map<int,int> primes;
while(n--)
{
int x;
cin >> x;
//分解这个输入的x
for(int i = 2; i <= x / i;i++)
while(x % i == 0)
{
x /= i;
primes[i] ++;//i这个质因数的指数加1
}
if(x > 1) primes[x] ++;
}
//结合公式输出
LL res = 1;
for(auto prime : primes)
{
//p是质数,也就是公式中的底数,a是质数出现的个数,也就是公式中的指数
int p = prime.first , a = prime.second;
LL t = 1;
while(a--) t = (t * p + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
🌻算法模板
求约数和算法实现流程:
🌻公式推导
看完求约数个数的算法实现流程和求约数和的算法实现流程小伙伴们心中应该能很清晰的感受到,大致是相似的,只是最后输出时运用的公式不一样。
数论这章了,很像高中的数学题了,知道那个性质,明白那个公式,大抵就能够做出来的
在查阅资料之后,发现李煜东老师的《算法竞赛——进阶指南》中的对两个公式的推导十分清晰,故笔者就不画蛇添足、班门弄斧了
笔者就结合例题来具体演示喔
求约数个数的公式的意思,就是将得到的质因数再分解。例如25中,20是约数,21是约数,22是约数,23是约数,24是约数,25是约数,所以就有5+1个约数,即公式中的c1+1。对于其他分解出来的质因数也是同理分解。
求约数和的公式意思,将分解出来的质因数,同时也是这个正整数乘积96的约数,它们之间的和是多少。
25 和 31 组成的约数应该有:20、 21 、 22、23 、 24 、25 、 30 、 31。
那么这些它们组成的约数和应试是
20 x ( 30 + 31 ) + 21 x ( 30 + 31)+…+ 2^5 x ( 30 + 31 )
经过整理,就可以得到形容李老师书中的公式:
( 20+ 21 + 22+ 23 + 24 +25 ) x( 30 + 31)
即:(1+ 21 + 22+ 23 + 24 +25 )x (1 + 31)
演示完毕啦
🌟欧几里得算法
🌻基本概念
公约数:
若自然数 d 同时是自然数 a 和 b的公约数。则称 d 是 a 和 b 的公约数。在所有 a 和 b 的公约数中最大的一个,称为 a 和 b 的最大公约数,记作 gcd(a,b) |
公倍数:
若自然数 m 同时是自然数 a 和 b的公约数。则称 m 是 a 和 b 的公倍数。在所有 a 和 b 的公约数中最小的一个,称为 a 和 b的最小公倍数,记作 lcm(a,b) |
🌻例题描述
🎇🎇🎇原题传送门🎇🎇🎇
🌻参考代码(C++版本)
#include <iostream>
using namespace std;
//欧几里得算法,亦称为辗转相除法
int gcd(int a,int b)
{
return b ? gcd(b, a%b):a;
}
int main()
{
int n;
scanf("%d",&n);
while(n--)
{
int a, b;
scanf("%d%d",&a,&b);
printf("%d\n",gcd(a,b));
}
return 0;
}
🌻算法模板
通俗来说,就把b定为除数,只要它还能够进行除法运算,就把它抛到函数中,与a进行除法运算,所有欧几里得算法也叫辗转相除法。 |
💓总结
一、质数、约数、公约数、公倍数这些我们曾经学过的数学知识要清楚喔。后续质数的筛选、分解,约数的统计都是在这些基础知识上开展的。 |
二、对于数论的题,小伙伴假如把我之前写的图论的文章和这篇对比,应该可以很清楚的感受到,算法模板这块的内容不再是清爽的算法的执行流程了,取而代之的是一两行公式的代码或者公式的推导理解。因为数论的题就和数学十分贴贴了,对公式的理解和运用就转变为了核心。所以需要自己下来落实对公式的推导,加深理解,才更好记忆住,最后在赛场上,大杀四方 |