简单数论(1)
- 前言
- 一. 质数筛
- 1.什么是质数筛
- 2.指数筛的种类
- 3.朴素方法
- 4.埃氏筛法
- 5.欧拉筛法(线性筛法)
- 总结
前言
我会将一些常用的算法以及对应的题单给写完,形成一套完整的算法体系,以及大量的各个难度的题目,目前算法也写了几篇,滑动窗口的题单正在更新,其他的也会陆陆续续的更新,希望大家点赞收藏我会尽快更新的!!!
一. 质数筛
1.什么是质数筛
质数:质数又称素数,是指在大于 1 的自然数中,除了 1 和它本身以外不再有其他因数的自然数。例如,2、3、5、7、11 等都是质数。
顾名思义质数筛就是通过某种方法将其质数给筛出来,对于某些要对质数进行操作的题目很有帮助。
2.指数筛的种类
基本的质数筛有三种:
1.朴素方法:时间复杂度为O(n ^ 2),但经过优化可以达到O(n ^(3 / 2))。
2.埃氏筛法:时间复杂度为O(n log logn)。
3.欧拉筛法:时间复杂度为O(n),因为其时间复杂度为O(n),所以又叫线性筛法。
以上三种方法时间复杂度逐渐降低,但代码逐渐抽象。
3.朴素方法
朴素方法就是暴力求解通过两层for循环来找到所有质素,其代码为:
#include <iostream>
using namespace std;
int n, k;
int prime[10005];
bool isPrime(int nums) {
for (int i = 2; i <= nums; i++) {
if (nums % i == 0) {//从二开始,看看有没有能被其整除的,若有则不是质数
return false;
}
}
return true;
}
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {//从i= 2开始,因为1不是质数
if (isPrime(i)) {
prime[k++] = i;
}
}
for (int i = 0; i < k; i++) {
cout << prime[i] << endl;
}
return 0;
}
** 第一次简化:**从质数的定义我们知道,如果一个数(n)不是质数那么它就可以由两个数相乘得到 (这两个数都不能是1),那么两个数最小的一个数都要大于2,另一个数必然<= n / 2。所以我们只要求n在[2, n / 2]范围内能不能被整除就可以知道n是不是质数。所以第一次简化的代码为:
#include <iostream>
using namespace std;
int n, k;
int prime[10005];
bool isPrime(int nums) {
for (int i = 2; i <= nums / 2; i++) {//注意是i <= nums / 2,不是i < nums / 2,如果是后者就会多个4,导致答案错误
if (nums % i == 0) {//从二开始,看看有没有能被其整除的,若有则不是质数
return false;
}
}
return true;
}
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {//从i= 2开始,因为1不是质数
if (isPrime(i)) {
prime[k++] = i;
}
}
for (int i = 0; i < k; i++) {
cout << prime[i] << endl;
}
return 0;
}
虽然范围减少了一半,但其时间复杂度还是O(n)。
所以来第二次简化:
还是从质数的定义入手,如果一个数(n)不是质数那么它就可以由两个数相乘得到 (这两个数都不能是1),那么我们来看看这两个数有什么特点,如图:
我们不难发现无论这个数有几种分法,但其中一个数是肯定小于等于根号n的,所以我们可以简化成,下面这串代码:
#include <iostream>
#include <cmath>
using namespace std;
int n, k;
int prime[10005];
bool isPrime(int nums) {
for (int i = 2; i <= sqrt(nums); i++) {//sqrt()函数为求一个数的平方根运算
if (nums % i == 0) { //注意这里也是<=因为4,9...也不行
return false;
}
}
return true;
}
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
if (isPrime(i)) {
prime[k++] = i;
}
}
for (int i = 0; i < k; i++) {
cout << prime[i] << endl;
}
return 0;
}
此时,代码的时间复杂度为O(n ^ (3 / 2))。虽然时间复杂度被优化了,但面对一些题目时间复杂度还是太高了,那我们来看看时间复杂度更低的代码。
4.埃氏筛法
了解埃氏筛法之前我们先了解一个定理:
算数基本定理(唯一分解定理):任何合数(除1和质数外的正整数)都可以表示为若干个质数的乘积,该分解式是唯一的。
而埃氏筛法就是利用算术基本定理来求解的:
我们可以从最小的质数开始(即2),将它的倍数都标记为合数,然后不断重复这个过程,直到遍历完所有小于等于目标数平方根的数,剩下未被标记的数就是质数。
也就是从最小的质数开始每次乘以i(2,3,4…)倍,将不是质数的数全部找出来,当>= n时停止,从下一个质数开始继续找。其代码如下:
#include <iostream>
using namespace std;
int Prime[10005];//全局变量初始化为0,且Prime[i] = 0为质数Prime[i] = 1不是质数
int isPrime[10005];//将质数存入这个数组
int n, k;
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
if (Prime[i] == 0) {
isPrime[k++] = i;
for (int j = i * 2; j < n; j += i) {//质数的所有倍数都不是质数
Prime[j] = 1;
}
}
}
for (int i = 0; i < k; i++) {
cout << isPrime[i] << endl;
}
return 0;
}
此时代码的时间复杂度为O(n log log n),已经很低了,但是还能再低。
5.欧拉筛法(线性筛法)
我们观察上面的代码发现其中有好多数字被判断了好几次(如:6,可以看成2的3倍,也可以看成3的2倍),那么我们能不能只判断一次呢:
如果有一种方法可以将一个非质数分成两个特定的数就好了,那这两个数要么都是合数,要么一个是合数一个是质数,我们又发现,埃氏筛法的筛除方法使用的质数是从小到大的,那么我们可以把这个数分成一个最小的质数和一个合数,具体代码如下:
#include <iostream>
using namespace std;
int Prime[10005];
int isPrime[10005];
int n, k;
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
if (isPrime[i] == 0) {
Prime[k++] = i;
}
for (int j = 0; j < k; j++) {
int x = i * Prime[j];
if (x > n) {
break;
}
isPrime[x] = 1;
if (i % Prime[j] == 0) {//前面都没有==0,到这才==0,说明i的最小质数是Prime ,那么后面的x = i * Prime[j1],其最小质因数必然也是Prime[j],后续这些合数会在之后的筛选中由更小的i值和Prime[j]筛去,为避免重复筛选,此时应停止。
break;
}
}
}
for (int i = 0; i < k;i++) {
cout << Prime[i] << endl;
}
return 0;
}
总结
以上3个质数筛中欧拉筛法时间复杂度最低,所以一般情况下建议大家使用欧拉筛法。