数论——质数和合数及求质数
质数和合数及求质数
一个大于 1 的自然数,除了 1 和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。其中,质数又称素数。有的资料用的词不同,但质数和素数其实是一回事。
规定 1 既不是质数也不是合数。
试除法判断质数
- 对于一个数 x x x,根据定义,可以从 [ 2 , x − 1 ] [2, x - 1] [2,x−1] 一个一个尝试,判断 x x x 能否被整除。
但是,没有必要每一个都去判断。因为 a a a 如果是 x x x 的约数,那么 x / a x / a x/a 也是 x x x 的约数。因此,我们仅需判断较小的 a a a 是否是 x x x 的约数,没有必要再去看看 x / a x / a x/a。那么,仅需枚举到 x \sqrt{x} x 即可。
试除法实现:
bool prim(int x) {if (x < 2)return false;for (int i = 2; i <= x / i; i++)//i*i<=x,所以i<=x/i,这是防溢出写法if (x % i == 0)return false;return true;
}
时间复杂度:因为是枚举到 n \sqrt{n} n,所以时间复杂度为 O ( N ) O(\sqrt{N}) O(N)。
模板题:
P5736 【深基7.例2】质数筛 - 洛谷
#include<bits/stdc++.h>
using namespace std;bool prim(int x) {if (x < 2)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 x = 0; n--;) {cin >> x;if (prim(x))cout << x << ' ';}return 0;
}
Eratosthenes筛选法(埃氏筛)
模板题:P3383 【模板】线性筛素数 - 洛谷
在很多场景我们需要知道[2, n]
内有多少个质数,或正数、倒数第 k k k个质数是多少。 n n n 很有可能特别大,此时传统的试除法一个一个判断显然会很慢甚至无法在规定时间内完成判断。因此需要使用其他算法来筛选出指定范围内的质数。
Eratosthenes筛选法基本思想:质数的倍数一定不是质数。
实现方法:用一个长度为 N + 1 N+1 N+1 的数组vis
保存信息(0 表示质数,1 表示非质数),先假设所有的数都是质数(初始化为 0),从小到大枚举每一个质数 x x x,把 x x x 的倍数都标记为非质数(置为 1)。当从小到大扫描时扫描到一个数 x x x ,若它尚未被标记,则它不能被 2 ∼ x − 1 2 \sim x-1 2∼x−1 之间的任何数整除,该数就是质数。
整数 1 特殊处理即可。在筛数的时候可以从 x 2 x^2 x2开始筛,因为小于 x x x的合数是被筛选过的。
//Eratosthenes筛(埃氏筛)
void get_prim(vector<int>&vis, vector<int>&ans) {for (size_t i = 2; i < vis.size(); i++) {if (vis[i])continue;//记录素数ans.push_back(i);//从i*i开始,因为小于i的合数已经被筛掉了for (size_t j = i * i; j < vis.size(); j+=i)vis[j] = true;//筛掉合数}
}
时间复杂度(推荐直接记结论): O ( n log log n ) O(n\log\log n) O(nloglogn)。这里 n n n实际就是vis.size()
。
严谨的数学推导:埃氏筛法的时间复杂度的详细证明-CSDN博客
P3383 【模板】线性筛素数 - 洛谷参考程序(埃氏筛):
#include<bits/stdc++.h>
using namespace std;void Eratosthenes(vector<bool>& vis, vector<int>& ans) {for (size_t i = 2; i < vis.size(); i++) {if (vis[i])continue;ans.push_back(i);for (size_t j = i * i; j < vis.size(); j += i)vis[j] = true;}
}int main() {int n, q,num;scanf("%d %d", &n, &q);vector<bool>vis(n + 1, false);vector<int>ans;Eratosthenes(vis, ans);//埃氏筛while (q--) {scanf("%d", &num);cout << ans[num - 1] << endl;}return 0;
}
线性筛(欧拉筛)
线性筛法,又称欧拉筛法。在埃氏筛法中,它会将一个合数重复多次标记。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 O ( n ) O(n) O(n) 。
我们的做法是,让每一个合数被它的最小质因数筛掉。
void get_prim(vector<int>& vis, vector<int>& ans) {for (size_t i = 2; i < vis.size(); i++) {if (!vis[i])//被标记为true时是合数ans.push_back(i);//1uLL是为了让size_t强制转换成unsigned long long防止溢出for (size_t j = 0; 1uLL * i * ans[j] < vis.size(); j++) {//筛掉合数vis[i * ans[j]] = true;//i是质数的倍数时停止,当i正好为最后一个质数时循环终止if (i % ans[j] == 0)break;}}
}
i%ans[j]==0
这个判定条件能让每一个合数被自己的最小质因数筛掉。
- 如果
i
是合数,枚举到最小质因数的时候跳出循环 - 如果
i
是质数,枚举到自身时跳出循环
注意,在筛的过程中,我们还能知道p[j]
是i
的最小质因数
这个算法最多遍历2遍数组,也就是说实际的时间复杂度是 O ( 2 n ) O(2n) O(2n)。
很多和数学有关的算法都是在欧拉筛的基础上实现(例如积性函数),因此欧拉筛很重要,需要理解这个算法的本质。
P3383 【模板】线性筛素数 - 洛谷参考程序(欧拉筛):
#include<bits/stdc++.h>
using namespace std;void Euler(vector<bool>& vis, vector<int>& ans) {for (size_t i = 2; i < vis.size(); i++) {if (!vis[i])ans.push_back(i);for (size_t j = 0; 1uLL * i * ans[j] < vis.size(); j++) {vis[i * ans[j]] = true;if (i % ans[j] == 0)break;}}
}int main() {int n, q,num;scanf("%d %d", &n, &q);vector<bool>vis(n + 1, false);vector<int>ans;Euler(vis, ans);//欧拉筛while (q--) {scanf("%d", &num);cout << ans[num - 1] << endl;}return 0;
}
质数有关OJ列举
P1835 素数密度 - 洛谷
P1835 素数密度 - 洛谷
因为 1 ≤ L ≤ R ≤ 2 31 1\leq L\leq R\leq 2^{31} 1≤L≤R≤231,无论是埃氏筛还是线性筛,在空间上会超限,在时间上遍历 2 31 2^{31} 231, O ( n ) O(n) O(n)的算法也无法在1秒内完成。
根据试除法,求一个数 r r r是否是质数,只需枚举 [ 1 , r ] [1, \sqrt{r}] [1,r]。
所以代码思路:
- 先找出 [ 1 , R ] [1, \sqrt{R}] [1,R]内的质数,再利用这些质数将 [ L , R ] [L,R] [L,R]内的合数标记。 R \sqrt{R} R的数据范围是 [ 2 , 2 16 ] [2,2^{16}] [2,216], 2 16 = 65536 2^{16}=65536 216=65536,在可接受范围内,用埃氏筛或欧拉筛都可以。
- 找到这些质数在 [ L , R ] [L,R] [L,R]内的最小合数后,用埃氏筛的做法标记 [ L , R ] [L,R] [L,R]内的其他合数。首先需要找到质数在 [ L , R ] [L,R] [L,R]的最小倍数。
所以整体思路是埃氏筛 + 欧拉筛。
找质数在 [ L , R ] [L,R] [L,R]内的最小倍数:假设 p ∈ [ 1 , R ] p\in [1,\sqrt{R}] p∈[1,R]且 p p p是质数,则 k p ≥ L kp\geq L kp≥L, k ≥ ⌈ L p ⌉ k\geq \lceil\frac{L}{p}\rceil k≥⌈pL⌉,也就是说可以通过左端点除以质数并向上取整,再乘质数,即可得到最小倍数。即 ⌈ L p ⌉ × p \lceil\frac{L}{p}\rceil\times p ⌈pL⌉×p。
在计算机中求 ⌈ L p ⌉ \lceil\frac{L}{p}\rceil ⌈pL⌉,特别是c++默认整数除法/
会去除小数部分,所以可以通过对 L + ( p − 1 ) p \frac{L+(p-1)}{p} pL+(p−1)向下取整来间接求得 ⌈ L p ⌉ \lceil\frac{L}{p}\rceil ⌈pL⌉。
- 若 L L L是 p p p的倍数,后面的 p − 1 p-1 p−1对整体不构成影响;
- 若不是,则在浮点数的视角, L p \frac{L}{p} pL会残留有小数部分,这一部分加上 p − 1 p \frac{p-1}{p} pp−1大于1,所以通过 ⌊ L + ( p − 1 ) p ⌋ \lfloor\frac{L+(p-1)}{p}\rfloor ⌊pL+(p−1)⌋也能间接求得 ⌈ L p ⌉ \lceil\frac{L}{p}\rceil ⌈pL⌉。
综上, ⌊ L + ( p − 1 ) p ⌋ = ⌈ L p ⌉ \lfloor\frac{L+(p-1)}{p}\rfloor=\lceil\frac{L}{p}\rceil ⌊pL+(p−1)⌋=⌈pL⌉。在代码中可通过(L+p-1)/p*p
得到质数 p p p的不小于 L L L的最小倍数。
其中需要注意的细节:
- L = 1 L=1 L=1,但1不是质数,所以此时需要从 L = 2 L=2 L=2开始。
- L ∈ [ 1 , R ] L\in [1,\sqrt{R}] L∈[1,R]且 L L L可能是质数。
例如{2,3,5}
,要求筛掉[5,25]
中的合数, ⌊ 5 + 5 − 1 5 ⌋ × 5 = 5 \lfloor\frac{5+5-1}{5}\rfloor\times 5=5 ⌊55+5−1⌋×5=5,因此会把5筛掉,但5是质数不能被筛掉,因为之前较小的质数已经把部分合数给晒掉了,所以若枚举到质数 p p p,则从 2 p 2p 2p开始筛,因此在使用埃氏筛的方法筛掉合数时可以从 max ( 2 p , ⌊ L + ( p − 1 ) p ⌋ ) \max(2p,\lfloor\frac{L+(p-1)}{p}\rfloor) max(2p,⌊pL+(p−1)⌋)开始。 - 因为 R R R是有可能到 2 31 2^{31} 231,数组不可能开这么大,但可以通过小区间
[0,R-L]
来存储[L,R]
的结果,再开一个 10 6 10^6 106的数组勉强能接受。 - 因为数据量整体过大,只有放弃
vector
转用一般的数组才能不超时。
#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1e6 + 10;
bool vis1[N], vis2[N];//因为超时,不得不换成普通数组
int ans[N], pans;
int L, R;void get_prim() {size_t n = sqrt(R);for (size_t i = 2; i <= n; i++) {if (!vis1[i])ans[++pans]=i;for (size_t j = 1; 1ull * i * ans[j] <= n; j++) {vis1[i * ans[j]] = 1;if (i % ans[j] == 0)break;}}
}int main() {cin >> L >> R;get_prim();L = L == 1 ? 2 : L;for (int i = 1; i <= pans;i++) {int x = ans[i];for (LL i = max(x * 2, (L + x - 1) / x * x);i<=R; i+=x) {vis2[i - L] = 1;}}int cnt = 0;for (int i = L; i <= R; i++) if (!vis2[i - L])++cnt;cout << cnt;return 0;
}
简单的哥赫巴德猜想和cin优化
UVA543 Goldbach’s Conjecture - 洛谷
1622:Goldbach’s Conjecture
这题是UVA上的题,但可以在信奥网站提交。
素数筛制表,然后查表即可。因为这个猜想至今仍然没有找到一个反例,所以直接找即可。
因为数据量过大,用cin
输入时需要使用优化手段,或直接用scanf
。而且不能用endl
。
#include<bits/stdc++.h>
using namespace std;const int N = 1e6 + 1;
bool vis[N];
int ans[N], pans;void get_prim() {for (int i = 2; i < N; i++) {if (!vis[i])ans[++pans] = i;for (int j = 1; 1ull*i * ans[j] < N; j++) {vis[i * ans[j]] = 1;if (i % ans[j] == 0)break;}}
}int main() {ios::sync_with_stdio(0);cin.tie(0);//不用这个对cin进行优化会超时//cout.tie(0);//这个有没有都不影响get_prim();int n;while (cin>>n, n) {for (int i = 1; i <= pans &&ans[i]<n; i++) {if (!vis[n - ans[i]]) {cout << n << " = " << ans[i] << " + " << n - ans[i] << "\n";break;}}}return 0;
}