一、知识和经验
把质数和约数放在一起就是因为他们有非常多的联系,为了验证这个观点我们可以先学习唯一分解定理:一个大于 1 的自然数一定能被唯一分解为有限个质数的乘积。
而且一个数不仅能被质数分解,原本也应该被自己的约数分解,所以在数论的题目中,一个或多个数被分解的题型可以是让你求被质数分解和被约数分解。所以他们两个放在一起既是相似,也可以起到对比防止混淆。
这篇文章我更倾向于写成质数与约数的对比文章,所以筛质数这个操作我会放到文章最后介绍。
二、判断质数
质数从 2 开始,并且除了 1 和本身没有约数。优化:只试 [2, 根号x]
bool isprime(int x)
{
if(x <= 1)
return false;
for(int i = 2; i <= x / i; i++)
if(x % i == 0)
return false;
return true;
}
例题:质数筛
// https://www.luogu.com.cn/problem/P5736
#include <bits/stdc++.h>
using namespace std;
bool isprime(int x)
{
if(x <= 1)
return false;
for(int i = 2; i <= x / i; i++)
if(x % i == 0)
return false;
return true;
}
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i++)
{
int x;
cin >> x;
if(isprime(x))
cout << x << ' ';
}
return 0;
}
三、分解质数和分解约数(一个数)
1、分解质数
其实就是实现唯一分解定理,也就是短除法一个个质数试。
这里有两种循环方法:
(1)[2, 根号x] 中的质数进行短除,毋庸置疑肯定是对的。
(2)直接 [2, 根号x] 循环试,这方法无非就是把合数也进行了循环,但是细想就会发现合数对应的分解质数一定出现在合数之前,所以在遇到合数的时候对应的质数已经短除完了,合数就算进入循环也不会产生影响
虽然方法二更简单,但是效率没有方法一高(实测过)
// 方法1
void deprime(int x)
{
for(int i = 2; i <= x / i; i++)
{
if(isprime(i))
{
while(x % i == 0)
{
x /= i;
a[i]++;
}
}
}
if(x > 1)
a[x]++;
}
// 方法2
void deprime(int x)
{
for(int i = 2; i <= x / i; i++)
{
while(x % i == 0)
{
x /= i;
a[i]++;
}
}
if(x > 1)
a[x]++;
}
a[i] 就是质数 i 对应公式中的指数
2、分解约数
分解约数很简单,就是 [1, 根号x] 试数
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n / i; i++)
{
if(n % i == 0)
cout << i << ' ';
if(i * i != n)
cout << n / i << ' ';
}
return 0;
}
3、对比
都是试数,但是质数范围 [2, 根号x],约数范围 [1, 根号x]
四、分解质数和分解约数(多个数)
1、暴力解法
既然一个数都能分解,那多个数分解的暴力解法就是枚举所有数,然后把所有数分解。
例题:质因子分解
// https://www.luogu.com.cn/problem/P2043
// 质因子分解:模拟短除法,[2, 根号n]一个数除完之后下一个,因为合数一定被前面的质数分解完了,所以可以直接枚举,反正不会有合数
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
int a[N];
void deprime(int x)
{
for(int i = 2; i <= x / i; i++)
{
while(x % i == 0)
{
x /= i;
a[i]++;
}
}
if(x > 1)
a[x]++;
}
int main()
{
int n;
cin >> n;
for(int i = 2; i <= n; i++)
{
deprime(i);
}
for(int i = 2; i <= n; i++)
if(a[i])
cout << i << ' ' << a[i] << endl;
return 0;
}
2、分解质数优化
例题:阶乘分解
分组思想:质数 x 在阶乘中的分解就是 x 的次方在 [1, n] 中的分组个数总和。
// https://www.luogu.com.cn/problem/P10495
#include <bits/stdc++.h>
using namespace std;
#define ll long long
// 枚举质数及其次方来达到统计出现多少质数的目的
const int N = 1e6 + 10;
int a[N];
bool isprime(int x)
{
if(x <= 1)
return false;
for(int i = 2; i <= x / i; i++)
if(x % i == 0)
return false;
return true;
}
int main()
{
int n;
cin >> n;
// 枚举质数
for(int i = 2; i <= n; i++)
{
if(isprime(i))
{
// 枚举质数及其次方统计
ll p = i;
while(p <= n)
{
a[i] += n / p;
p *= i;
}
}
}
for(int i = 2; i <= n; i++)
if(a[i])
cout << i << ' ' << a[i] << endl;
return 0;
}
3、分解约数优化
例题:约数个数的和
分组思想:一个数 x 作为约数在 [1, n] 中的个数就是分组之后的组数。
// https://ac.nowcoder.com/acm/problem/14682
// 1~n所有约数的和:反过来枚举约数即可
// 对于一个x,在[1, n]中有n / x个数的约数有x
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int main()
{
int n;
cin >> n;
ll ans = 0;
for(int i = 1; i <= n; i++)
ans += n / i;
cout << ans;
return 0;
}
五、唯一分解定理在约数中的应用
唯一分解定理中的质数 p 和对应的指数 a 对于约数是有用的。
一个数的约数个数等于 ai 加一之后全部的乘积
一个数的约数之和等于 pi 从指数 0 到 ai 的总和的全部乘积
例题:约数之和
// https://ac.nowcoder.com/acm/problem/22196
// 1、枚举全部约数相加
// 2.质因数分解之后用pa
// n的质因数不一定就比n小,可以相等
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 10;
int a[N];
void deprime(int x)
{
for(int i = 2; i <= x / i; i++)
{
while(x % i == 0)
{
x /= i;
a[i]++;
}
}
if(x > 1)
a[x]++;
}
int main()
{
int n;
cin >> n;
deprime(n);
ll ans = 1;
for(int i = 2; i <= n; i++)
{
if(a[i] != 0)
{
ll tmp = 1;
ll x = 1;
for(int j = 0; j < a[i]; j++)
{
x *= i;
tmp += x;
}
ans *= tmp;
}
}
cout << ans;
return 0;
}
六、质数筛
暴力解法就是每一个数判断是不是质数。
接下来学的两个算法一定一定是要掌握原理!!!!!
而不是说我会写 [1, n] 里面两种筛质数方法的代码就可以的!!!!!
怎么筛出来的一定一定要搞清楚!!!!不然遇到新题就是不会
1、埃氏筛
(1)原理
质数的倍数一定不是质数。
(2)代码实现
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e8 + 10;
bool st[N];
ll cnt = 0;
int p[N];
ll n, q;
// 埃氏筛
// 获取[1, n]中所有素数存储在p
// 思路:一个素数的倍数一定不是素数,即 1 * p, 2 * p...都可以筛掉
// 优化:可以直接从 p * p 开始筛,之前的[1, p - 1]被之前的数筛掉了
void agetprime()
{
for (ll i = 2; i <= n; i++)
{
if (st[i] == false)
{
p[cnt++] = i;
for (ll j = i; j * i <= n; j++)
st[i * j] = true;
}
}
}
i 用于枚举所有质数,j 枚举对应质数的倍数,两者相乘就是被筛掉的数
2、欧拉筛
(1)原理
模拟埃氏筛就会发现同一个合数可能会被筛掉很多次,如果一个数只会被筛掉一次,那么时间复杂度就会是 O(N),怎么做到呢?一个合数只会被他的最小质因子筛掉。
代码模拟逻辑就是:2~n 的所有数 i 都要遍历,对应的倍数 j 是 [2, i] 中所有的质数,两者相乘的数被筛掉,如果 i 是 j 的倍数,终止循环。
(2)代码实现
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e8 + 10;
bool st[N];
ll cnt = 0;
int p[N];
ll n, q;
// 欧拉筛
// 在埃氏筛中许多合数会被多次置true,如果每个合数只会被筛一次那么就是O(N)
// 也就是每一个合数只会被自己的最小质因子筛掉,12 = 2 * 2 * 3,那么12只被2筛掉一次
// 做到2点即可:埃氏筛中的j是从质数表中遍历,且一旦i能整除j就停止循环
// 上述的j是一个质数,代码中由于要循环遍历,所以j是下标,p[j]对应质数
// 这里由于每一个合数只会被自己的最小质因子筛掉,所以后面的合数可能会被前面的合数乘上质数列表中的一个数给筛掉
// 所以每一个数都要遍历到,不像埃氏筛中只遍历质数
void ogetprime()
{
for (ll i = 2; i <= n; i++)
{
if (st[i] == false)
p[cnt++] = i;
for (int j = 0; i * p[j] <= n; j++)
{
// 如果i是合数,遍历到最小质因子结束
// 如果i是质数,遍历到他自己结束
// 所以判断成立的时候p[j]就是i的最小质因子
st[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
}
对应例题:线性筛质数
3、对比
埃氏筛基于的是质数,只要给我 [2, n] 区间内的质数,我就能筛出 [2, n * n] 的质数,因为质数的倍数不是质数,即用一段区间的质数筛质数。
欧拉筛基于的是当前数之前所有的质数,然后我当前数乘以遍历质数去筛掉后面的数,即用连续的一段区间的质数筛质数。
一个是可以用毫无关联的一组质数筛质数,一个是必须连续的筛 [2, n] 质数。
4、例题
(1)素数密度
// https://www.luogu.com.cn/problem/P1835
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
#define ll long long
int p[N];
int cnt = 0;
bool st[N];
// 欧拉筛+线性筛
// 欧拉筛出[2, sqrt(r)]
void ogetprime(int n)
{
for(ll i = 2; i <= n; i++)
{
if(st[i] == false)
p[cnt++] = i;
for(int j = 0; i * p[j] <= n; j++)
{
st[i * p[j]] = true;
if(i % p[j] == 0) break;
}
}
}
bool ans[N];
int main()
{
int l, r;
cin >> l >> r;
if(l < 2)
l = 2;
int limit = sqrt(r);
ogetprime(limit);
// 埃氏筛:现在已经有[2, sqrt(r)]所有质数,每个质数的倍数不是质数
// 以前没有范围时倍数从p[i]开始,现在有l左边界,倍数从左边界向上整除p[i]或者至少2,向上整除?(l + x - 1) / x
for(int i = 0; i < cnt; i++)
{
ll x = p[i];
for(ll j = max(x * 2, (l + x - 1) / x * x); j <= r; j += x)
{
ans[j - l] = true;
}
}
int sz = 0;
for(int i = l; i <= r; i++)
if(ans[i - l] == false)
sz++;
cout << sz;
return 0;
}
要注意左边界可能为1,此时变成2
如果左边界是质数,仅仅是向上取整还不够,倍数必须是2才能避免质数被筛掉。
(2)哥德巴赫猜想
UVA543 Goldbach's Conjecture - 洛谷
// https://www.luogu.com.cn/problem/UVA543
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 10;
int p[N];
int cnt = 0;
bool st[N];
void ogetprime()
{
for(ll i = 2; i <= N; i++)
{
if(st[i] == false)
p[cnt++] = i;
for(ll j = 0; p[j] * i <= N; j++)
{
st[p[j] * i] = true;
if(i % p[j] == 0) break;
}
}
}
void f(int x)
{
for(int l = 1; l < cnt; l++)
{
if(st[x - p[l]] == false)
{
printf("%d = %d + %d\n", x, p[l], x - p[l]);
return;
}
}
cout << "Goldbach's conjecture is wrong" << endl;
}
int main()
{
ogetprime();
int x;
while(1)
{
cin >> x;
if(x == 0)
break;
f(x);
}
return 0;
}