Appearance
数论入门 - 质数筛法
一、课程目标
- 质数查找问题
- 埃氏筛法
- 欧拉筛法
二、目标详解
质数查找问题
一般为求区间[L, R]里的质数个数。
素数:一个大于1的自然数,除了1和它本身以外不再有其他因数的数称为素数。 合数:一个大于1的自然数,除了1和它本身以外还有其他因数的数为合数。
1、枚举法
朴素算法:枚举(暴力),判断是否为质数,复杂度为O(n * sqrt(n))。
c++
//自定义判断函数
bool isPrime(int x) {
if(x < 2) return false;
//int t = sqrt(x); //由于下面的sqrt(x)会多次计算,所以可以先计算一次,省略一丢丢时间
for(int i=2; i<=sqrt(x); i++)
if(x%i == 0)
return false;
return true;
}
//逐一判断
for(int i=2; i<=n; i++)
if(isPrime(i))
cnt++;
2、埃氏筛法
埃氏筛法,全名埃拉托斯特尼筛法,是一种古老且简单的用来找出一定范围内所有的质数的算法,公元前250年由希腊数学家埃拉托斯特尼提出。
**核心思想:**质数的整数倍一定是合数,标记出所有的合数,剩下的就是质数
筛选过程:
从小到大遍历每个数x,如果x是质数,就把x的倍数都进行标记,剩下的就是质数。
模拟过程:
i: | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|
2: | 2 | 3 | ~~4~~ | 5 | ~~6~~ | 7 | ~~8~~ | 9 | ~~10~~ |
3: | 2 | 3 | - | 5 | - | 7 | - | ~~9~~ | - |
... |
此算法用到了双重循环,但由于内层循环的步长为i,时间复杂度可看成O(nloglogn)。
c++
//埃氏筛
#include <bits/stdc++.h>
using namespace std;
bool isPrime[1000005];
int n;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
isPrime[i] = 1;
}
isPrime[0] = 0;
isPrime[1] = 0;
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
for (int j = i*2; j <= n; j+=i) { //标记质数的倍数
isPrime[j] = 0;
}
}
}
for (int i = 1; i <= n; i++) {
if (isPrime[i]) {
cout << i << " ";
}
}
return 0;
}
//使用vector代码1
for(int i=2; i<=n; i++) {
if (isPrime[i]) {//不是合数,也就是质数
primes.push_back(i);
for(int j=2*i; j<=n ; j=j+i)
isPrime[j] = false;
}
}
//代码2,一丢丢优化
for(int i=2; i*i<=n; i++) { //i*i能减少计算量,比如i为3的时候,3*2 为 6,其实 6 已经被 2*3 标记了,所以i*i
if (isPrime[i]) {//不是合数,也就是质数
primes.push_back(i);
for(int j=i*i; j<=n ; j=j+i)
isPrime[j] = false;
}
}
3、欧拉筛法
在筛法模拟里会看到存在大量的重复筛,比如6即被2筛掉、也被3筛掉, 如果每个合数都只被一个数筛掉,那么这个算法显然就是O(n)的算法,即线性筛法。
前置知识:
整数唯一分解定理:
- 任何大于1的正整数n都可以唯一分解为有限个质数的乘积;
- 任何合数都有他对应的一个最小质因子(合数=最小质因数×另一个数);
合数只通过这个最小质因子将其标记这样就避免了重复标记。
核心思想:
每个数都只被它的最小质因子筛掉。
筛选过程:
- 1、对每个数
,将小于等于 的质数乘以 标记为合数。 - 2、如果某个质数是
的因子,则停止(后面的不要筛了)
算法解释:
- 1、对于某个数
,假设已经找到 个质数: , 计划将 和 这些质数的乘积都筛掉。 - 2、但如果
存在一个质因子,假设为 ,即 那么对于 到 之间的任何质数 ,都有 也就是说后面肯定会有一个比 x
大的数y = (k*p)
,也会筛掉y * px = x * p
- 例子:6 以前的质数有 2,3,5 首先 6 * 2 = 12,那么 12 会进行合数标记,但是 6 * 3 = 18 并不会进行标记,因为还有更大的一个数,9 * 2 = 18,所以直接结束掉即可,同样的,如果是9,9之前的质数有2,3,5,7 筛选的 9*2=18,9*3=27之后便结束,因为9*4=36,但36可以由18来除,36=18*2
模拟过程:
i | 质数 | 筛 |
---|---|---|
2 | 2 | 4 |
3 | 2, 3 | 6, 9 |
4 | 2 ,3 | 8 |
5 | 2,3,5 | 10, 15, 25 |
6 | 2 ,3,5 | 12 |
7 | 2,3,5,7 | 14,21,35,49 |
8 | 2 ,3,5,7 | 16 |
9 | 2,3 ,5,7 | 18,27 |
... |
由于每个数都只有其最小质因子乘以某个x筛掉,所以这个算法为O(n)。
c++
//写法1
for(int i=2; i<=n; i++) {
if(isPrime[i]) primes.push_back(i);
for(int j=0; j<primes.size(); j++) {
int p = primes[j];
if(i * p > n) break;
isPrime[i * p] = false;
if(i%p == 0) break;
}
}
//写法2
for(int i=2; i<=n; i++) {
if(isPrime[i]) primes.push_back(i);
for(int j=0; j<primes.size() && i*primes[j] <= n; j++) { //这里的i刚好是充当了质数的不同倍数
isPrime[i * primes[j]] = false;
if(i % primes[j] == 0) break;
}
}
练习-找质数(枚举)
一、实验目标
输入n,找出<=n的所有质数。
输入格式
:
- 一个正整数n
输出格式
:
- 第一行一个数字,表示质数个数k
- 第二行k个质数,从小到大,空格隔开
输入样例1
:
text
10
输出样例1
:
text
4
2 3 5 7
数据范围
:
- n<=10^7
二、分析
1、思路
首先实现一个函数isPrime,用来判断某个数是否是质数。
然后枚举2..n,调用isPrime函数进行判断,如果是质数就加到一个动态数组。
最后输出动态数组的长度,和每个元素。
2、动态数组-下标法
可以用一个固定长度的数组和一个下标last来存储动态,如:
c++
int a[N], last;//N足够大
保存:
c++
a[++last] = i;
输出:
c++
cout<<last<<endl; //last是长度
for(int i=1; i<=last; i++)
cout<<a[i]<<" ";
3、动态数组-vector
也可以使用 STL
里提供的 vector
类,如:
c++
vector<int> primes;
保存时用 push_back
方法:
c++
primes.push_back(i);
输出:
c++
cout << primes.size() << endl; //长度用size()方法
for (int i=0; i<primes.size(); i++) //下标从0开始
cout << primes[i] << " ";
演示-找质数(埃氏筛法)
一、实验目标
输入n,找出1~n的所有质数。
输入格式
:
- 一个正整数n
输出格式
:
- 第一行一个数字,表示质数个数k
- 第二行k个质数,从小到大,空格隔开
输入样例1
:
text
10
输出样例1
:
text
4
2 3 5 7
数据范围
:
- n<=10^7
二、分析
1、思路
从2开始,每个质数往后筛自己的倍数,设置为合数。
筛完以后剩下的就是质数,将之存储到动态数组里,输出。
时间复杂度
:
2、桶数组
bool isPrime[N]
是桶数组,用来标记 i
是否为质数。
3、动态数组
用vector实现。
c++
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;
const int N = 1e7+5;
int n;
bool isPrime[N];
vector<int> primes;
int main() {
cin>>n;
for(int i=1; i<=n; i++)//默认都是质数
isPrime[i] = true;
//埃氏筛法:每个数筛自己的倍数
for(int i=2; i*i<=n; i++) {
if (isPrime[i]) {//不是合数,也就是质数
primes.push_back(i);
for(int j=i*i; j<=n ; j=j+i)
isPrime[j] = false;
}
}
cout<<primes.size()<<endl;
for(int i=0; i<primes.size(); i++)
printf("%d ", primes[i]);
cout<<endl;
return 0;
}
演示-找质数(欧拉筛法)
一、实验目标
输入n,找出<=n的所有质数。
输入格式
:
- 一个正整数n
输出格式
:
- 第一行一个数字,表示质数个数k
- 第二行k个质数,从小到大,空格隔开
输入样例1
:
text
10
输出样例1
:
text
4
2 3 5 7
数据范围
:
- n<=10^7
二、分析
1、思路
从2开始,每个数被最小的质因子筛掉。
时间复杂度
:O(n)
2、桶数组
bool isPrime[N]是桶数组,用来标记i是否为质数。
3、动态数组
用vector实现。
c++
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;
const int N = 1e7+5;
int n;
bool isPrime[N];
vector<int> primes;
int main() {
cin>>n;
for(int i=1; i<=n; i++)//默认都是质数
isPrime[i] = true;
//欧拉筛法:每个数x筛去x * <=最小质因子的质数
for(int i=2; i<=n; i++) {
if(isPrime[i])
primes.push_back(i);
for(int j=0; j<primes.size(); j++) {
int p = primes[j];
if(i*p > n) break;
isPrime[i * p] = false;
if(i%p == 0) break;
}
}
cout<<primes.size()<<endl;
for(int i=0; i<primes.size(); i++)
printf("%d ", primes[i]);
cout<<endl;
return 0;
}
练习-哥德巴赫猜想
题目描述
1742年6月7日哥德巴赫写信给当时的大数学家欧拉,正式提出了以下的猜想:任何一个大于9的奇数都可以表示成3个质数之和。
质数是指除了1和本身之外没有其他约数的数,如2和11都是质数,而6不是质数,因为6除了约数1和6之外还有约数2和3。需要特别说明的是1不是质数。这就是哥德巴赫猜想。
欧拉在回信中说,他相信这个猜想是正确的,但他不能证明。从此,这道数学难题引起了几乎所有数学家的注意。哥德巴赫猜想由此成为数学皇冠上一颗可望不可及的“明珠”。现在请你编一个程序验证哥德巴赫猜想。
输入输出格式
输入格式:
- 一行一个正奇数n
输出格式:
- 一行空格隔开三个质数(最后一个质数后无空格),如果方案不唯一,输出第一个数最小的方案,如果第一个数最小的方案仍不唯一,输出第二个数最小的方案。
输入输出样例
输入样例:
text
11
输出样例:
text
2 2 7
测试点
测试点:5个测试点,每个测试点得20分。
测试限制:每个测试点时间限制1s,内存限制128M。
数据范围:
- 40%的数据:9 < n < 10^5
- 80%的数据:9 < n < 10^6
- 100%的数据:9 < n < 10^7
习题
1061
2462
2136
2071
1549
2007
1942