数论——质数与约数
一、质数
【相关概念】
因数:一整数被另一整数整除,后者即是前者的因数,如1,2,4都为8的因数
倍数:一个数能够被另一数整除,这个数就是另一数的倍数。如15能够被3或5整除,因此15是3的倍数,也是5的倍数。
质数:一个数除了1和它本身没有其他的因数,就叫质数。如2,3,5,7,
和数:一个数除了1和它本身还有其他的因数,(至少有3个因数)就叫合数。如4,6,8,9
1.质数的判断
(1)质数的判断——枚举法
//根据质数定义判断 一个数是否为质数 O(n)
bool is_prime(int n) // 判定质数
{
if(n < 2) return false;
for (int i = 2; i < n; i ++ )
{
if(n % i == 0)
return false;
}
return true;
}
(2)质数的判断——试除法
上述短除法是从头到尾枚举判断,时间复杂度相对很高,其实没有必要枚举1~n所有的数然后判断!
思路:
把1~根号n
之间的数都找一遍,看看有没有一个数是n
的因子,之所以找到根号n
,是因为因子都是成对出现的,假设i
是n
的因子,那么在根号n
后面必定有一个数n/i
是n
的因子。(只枚举每一对因子中较小的数)。
i | n 那么 n/i | n ,即枚举i的范围从
2~n/i
即可(只枚举每一对因子中较小的数) i*i <= n 所以 i <= sqrt(n),即时间复杂度
O(sqrt(n))
//试除法 判断一个数是否为质数 O(sqrt(n))
bool is_prime(int n)
{
if(n < 2) return false;
for (int i = 2; i <= n / i; i ++ )
{
if(n % i == 0)
return false;
}
return true;
}
2.分解质因数
【相关概念】
什么是质因数?
如果一个数的因数是质数,那么这因数就是质因数!
什么是分解质因数?
把一个合数分解成若干个质因数的乘积的形式,即求质因数的过程叫做分解质因数。
(1)分解质因数——短除法
小学时分解质因数的方法——短除法
分解质因数只针对合数(质数就只有本身和1两个因数,不能分解)。一个数分解质因数,要从最小的质数除起(依次除于质数2 3 5....),一直除到结果为质数为止。分解质因数的算式叫短除法.
【参考代码】
短除法分解质因数——O(n)
void divide(int n)
{
for(int i = 2; i <= n; i ++)
{
if(n % i == 0)//i一定是质数
{
int s = 0;
while(n % i == 0)//短除法分解质因数
{
n /= i;
s ++;//统计i出现的次数
}
printf("%d %d\n", i, s);
}
}
}
(2)分解质因数——试除法
上述短除法分解质因数,要从最小的质数除起(依次除于质数2 3 5....)一直到n,时间复杂度为O(n)
,其实也没必要枚举1~n所有质数然后进行判断!
这里有一个重要的性质
n中最多只含有一个大于sqrt(n)的质因子。
证明:通过反证法:如果有两个大于sqrt(n)的因子,那么相乘会大于n,矛盾。证毕
于是我们发现最多只有一个大于sqrt(n)的因子,对其进行优化。先考虑比sqrt(n)小的,代码和质数的判定类似
最后如果n还是>1,说明此时的n就是大于sqrt(n)的唯一质因子,输出即可。(特判)
【参考代码】
试除法分解质因数——O(sqrt(n))
void divide(int n)
{
for(int i = 2; i <= n / i; i ++)
{
if(n % i == 0)//i一定是质数
{
int s = 0;
while(n % i == 0)//短除法分解质因数
{
n /= i;
s ++;//统计i出现的次数(指数)
}
printf("%d %d\n", i, s);
}
}
if(n > 1) printf("%d %d\n", n, 1);//当n没有变化的时候,输出本身和1
}
证明一下循环里面的
i
一定是一个质数:假如i
是一个合数,那么它一定可以分解成多个质因子相乘的形式,这多个质因子同时也是n
的质因子且比i
要小,而比i
小的数在之前的循环过程中一定是被条件除完了的,所以i
不可能是合数,只可能是质数
3.筛质数
求某个数n有多少个质数?
1.朴素筛法
枚举i:2~n
,从前往后把每个数对应的倍数都删除掉,这样筛过之后,所有剩下的数都是质数。(每个数i不存在该数的约数)
时间复杂度:O(nlogn)
int prime[N], cnt;//prime数组存储质数,cnt统计个数
bool st[N];//标记是否被筛过
//朴素筛法-O(nlogn)
void get_primes(int n)
{
for(int i = 2; i <= n; i ++)
{
if(!st[i])//如果没被筛过
{
prime[cnt ++] = i;
}
//把i的每一个倍数结果删掉
for(int j = i + i; j <= n; j += i) st[j] = true;
}
}
2.埃氏筛法
埃氏筛法是对朴素筛法的一种优化方式,我们并不需要把2~n的每一个数的倍数都删掉,可以只把所有质数的倍数删掉!
埃氏筛法:我们会发现4的倍数也是2的倍数,所有我们只需要把所有质数的倍数删除就好。
时间复杂度:O(n * loglogn)
,近似O(n)
//朴素筛法-O(n * loglogn)
void get_primes(int n)
{
for(int i = 2; i <= n; i ++)
{
if(!st[i])//如果没被筛过
{
prime[cnt ++] = i;
//把质数i的每一个倍数结果删掉
for(int j = i + i; j <= n; j += i) st[j] = true;
}
}
}
3.线性(欧拉)筛法
埃氏筛法的缺陷 :对于一个合数,有可能被筛多次。例如 30 = 2 * 15 = 3 * 10 = 5*6……那么如何确保每个合数只被筛选一次呢?我们只要用它的最小质因子来筛选即可,这便是欧拉筛法。
欧拉筛法的基本思想 :在埃氏筛法的基础上,让每个合数只被它的最小质因子筛选一次,以达到不重复的目的。
int prime[N], cnt;
bool st[N];
// 合数 = 质数(最小) x 因数
//线性筛法-O(n)
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
// 如果没被筛过 将素数记下来
if(!st[i]) prime[cnt ++] = i;
// 从小到大枚举所有质数
for(int j = 0; i * prime[j] <= n; j ++)//primes[j]*i<=n,把大于n的合数都筛了就没啥意义了
{
// 每个合数只被它的最小质因子筛掉
st[i * prime[j]] = true;
if(i % prime[j] == 0) break;// prime[j]一定是(合数)i的最小质因子(从小到大枚举质数嘛)
}
}
}
- i % prime[j] == 0
prime[j]一定是(合数)i的最小质因子,prime[j]也一定是prime[j] * i的最小质因子
i % prime[j] != 0
没有找到i的任何一个质因子,说明当前prime[j]一定小于i的所有质因子(质数是从小到大枚举的),prime[j]也一定是prime[j] * i的最小质因子
任何一个合数一定会被筛掉:
对于任何一个合数,它都会有且只有一个最小的质因子!因此只会被筛一次!
对比
二、约数
约束定义:
约数又称因数,整数A除以整数B(B≠0) 除得的商正好是整数而没有余数,我们就说A能被B整除,或B能整除A。A称为B的倍数,B称为A的约数。
质数只有两个约数:1和它本身。
合数至少有3个约数。
1.试除法求约数
思路:
O(sqrt(n))
的做法,枚举一个较小的因子,另一个直接用原数去除。
注 意 完 全 平 方 数 只 要 加 入 一 个 因 子 。
一个数
d
要是能被n
整除,那么n/d
也能被n
整除,即约数也是成对存在的,我们只需枚举较小的约数即可,另一个通过 n / d求出!从1
枚举到sqrt(n)
即可,即i <= n /i
。注:
当n为完全平方数时,约数只存储一次,不能够重复存储!因此要注意特判!
【代码实现】
vector<int> get_divisor(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;
}
【acwing 869 试除法求约数】
给定 nn 个正整数 aiai,对于每个整数 aiai,请你按照从小到大的顺序输出它的所有约数。
输入格式
第一行包含整数 nn。
接下来 nn 行,每行包含一个整数 aiai。
输出格式
输出共 nn 行,其中第 ii 行输出第 ii 个整数 aiai 的所有约数。
数据范围
1≤n≤1001≤n≤100,
2≤ai≤2×1092≤ai≤2×109输入样例:
2 6 8
输出样例:
1 2 3 6 1 2 4 8
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> get_divisor(int n)
{
vector<int> res;
//将成对的约数存下来,平方数只存一次
for(int i = 1; i <= n / i; i ++)
{
if(n % i == 0)
{
res.push_back(i);//较小的约数
//特判 防止重复存储n为平方数的情况
if(i != n / i) res.push_back(n / i);//存另一个约数 较大的约数
}
}
sort(res.begin(), res.end());
return res;
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
int a;
scanf("%d", &a);
auto res = get_divisor(a);
for(auto c : res) cout << c << " ";
puts("");
}
return 0;
}
for(auto x : arr) 遍历方式, x只是将arr里的元素复制下来,改变x不会改变arr的元素
for(auto &x : arr) x是将arr元素的地址拿出来,改变x会改变arr的元素
2.约数个数
求余数个数——百度百科
【acwing 870. 约数个数】
给定 nn 个正整数 aiai,请你输出这些数的乘积的约数个数,答案对 10^9+7 取模。
输入格式
第一行包含整数 nn。
接下来 nn 行,每行包含一个整数 aiai。
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对 109+7109+7 取模。
数据范围
1≤n≤1001≤n≤100,
1≤ai≤2×1091≤ai≤2×109输入样例:
3 2 6 8
输出样例:
12
分别对每一个ai分解质因数后,不断统计指数个数,不必将ai都相乘得结果后再分解质因数(数据可能过大溢出!)
【代码实现】
#include <iostream>
#include <cstring>
#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;
//分解质因数
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] ++;
}
//以上过程完成primes就存了所有质因数的指数
LL res = 1;
for(auto prime : primes) res = res * (prime.second + 1) % mod;
// 注:不能写成 res*= ... 这样就先取模再乘了(运算符优先级)
// 每次将乘积结果取模再进行下一次乘:可以保证中间结果不会溢出
cout << res;
return 0;
}
3.约数之和
求约数之和——百度百科
【acwing 871 约数之和】
给定 nn 个正整数 aiai,请你输出这些数的乘积的约数之和,答案对 10^9+7 取模。
输入格式
第一行包含整数 nn。
接下来 nn 行,每行包含一个整数 aiai。
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 109+7109+7 取模。
数据范围
1≤n≤1001≤n≤100,
1≤ai≤2×1091≤ai≤2×109输入样例:
3 2 6 8
输出样例:
252
【代码实现】
#include <iostream>
#include <cstring>
#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;
//分解质因数
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)
{
int p = prime.first, a = prime.second;
LL t = 1;
while(a --) t = (p * t + 1) % mod; //求出 p0一直加到p的k的次方的和 (结果可能很大先求模)
res = res * t % mod;
}
cout << res;
return 0;
}
约数个数与约数之和公式总结
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
4.最大公约数(欧几里得算法)
欧几里得算法也叫辗转相除法!
核心原理:
当b不为0时:
(a,b) = (b, a % b)
当b为0时:
(a,b)= a
【代码实现】
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
三、总结
学习内容源自:
百度百科
acwing算法基础课
注:如果文章有任何错误或不足,请各位大佬尽情指出,评论留言留下您宝贵的建议!如果这篇文章对你有些许帮助,希望可爱亲切的您点个赞推荐一手,非常感谢啦