有一个正整数 \(n\) ,试判断 \(n\) 是不是质数。
经典模板了属于是
主要有质数筛、枚举因子、Miller Rabin 算法三种做法
分为埃氏筛和欧拉筛(线性筛)两种
埃氏筛应该是判断质数的最基础方法了
从 \(2\) 开始从小到大依次枚举整数
如果没被筛过就说明是质数,同时应将其所有倍数(除本身外)筛去
v[1]=1; // 值为 1 表示非质数,0 表示是质数 for (int i=2; i<=n; ++i) if (!v[i]) for (int j=(i<<1); j<=n; j+=i) v[j]=1;
时间复杂度 \(O(n\log \log n)\)
欧拉筛同样是筛倍数,但用了一些技巧将时间复杂度降至了 \(O(n)\)
v[1]=1; for (int i=2; i<=n; ++i) { if (!v[i]) p[++cnt]=i; for (int j=1; j<=cnt&&p[j]*i<=n; ++j) { v[p[j]*i]=1; if (i%p[j]==0) break; } }
关键在于每个数恰好被其最小质因子筛一次
看懂这个结论之后 正确性和时间复杂度就都很显然了
质数的定义:只有两个正因数(\(1\) 和本身)的自然数是质数。
所以只需判断 \(2\sim n-1\) 中有没有 \(n\) 的因子
\(x\) 的因子是成对出现的,每一对因子中较小者必然不超过 \(\sqrt n\) ,因此从 \(2\) 枚举到 \(\sqrt n\) 即可
int isp(int x) { //判断质数,是则返回 1 ,否则返回 0 if (x==1) return 0; // 1 既不是质数也不是合数 for (int i=2; i*i<=x; ++i) if (x%i==0) return 0; return 1; }
时间复杂度为 \(O(\sqrt n)\)
细想一下发现仅枚举质因子即可保证正确性
所以有一种优化思路是尽量避免枚举合数
考虑如下结论:
大于 \(3\) 的质数必然和 \(6\) 的倍数相邻。
设 \(k\) 为正整数,则 \(6k-2,6k,6k+2,6k+3\) 显然都为合数
因此只需枚举形如 \(6k\pm 1(k\in\mathbb N^*)\) 的数即可
int isp(int x) { if (x<5) return (x==2||x==3); // 注意 2,3 需要特判 if (x%6!=1&&x%6!=5) return 0; for (int i=5; i*i<=x; i+=6) if (x%i==0||x%(i+2)==0) return 0; return 1; }
时间复杂度依然为 \(O(\sqrt n)\) ,但实际运行要比优化前快几倍,而且代码也不长,日常做题推荐使用
这篇文章讲得非常详细了:link
时间复杂度 \(O(k\log n)\) ,其中 \(k\) 为测试次数
OI 中可以选择固定的几个常数做测试,在保证准确的前提下:
\(2^{32}\) 以内选择 \(2,7,61\) 三个数即可
\(10^{16}\) 以内选择 \(2,3,7,13,61,24251\) 六个数即可
数据范围更大时还是建议换成随机数,具体实现可参考上面提到的文章
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
线性筛 | \(O(n)-O(1)\) | \(O(n)\) |
埃氏筛 | \(O(n\log \log n)-O(1)\) | \(O(n)\) |
暴力 | \(O(\sqrt n)\) | \(O(1)\) |
Miller-Rabin | \(O(k\log n)\) | \(O(k)\) |
\(O(n)-O(1)\) 表示预处理 \(O(n)\) ,回答单次询问 \(O(1)\) (埃氏筛时间复杂度同理)
如果数据范围较小,用暴力随便搞搞就行了
如果询问次数很多且 \(n\) 不是很大,应使用筛法(推荐线性筛)
Miller-Rabin 代码量大,写起来比较耗时,尽量少用
考场上很少会出现必须用 Miller-Rabin 的情况,所以这个算法不用练得太熟 差不多得了