质数相关问题
判断质数
如果一个正整数的因子只有1和他本身,那么该数为质数。质数必须大于1。因此我们可以对任意数n,从2到n-1穷举,如果这期间发现n的其他因子,那么该数就不是质数。
一个数被拆分成两个因子,如6=2* 3,那么只需要穷举2,不需要再穷举3,拆分成的两个因子是成对的。因此可以优化,只需要穷举到sqrt(n)即可。时间复杂度固定为O(sqrt(n))。
例题:https://www.acwing.com/problem/content/868/
#include <iostream>
#include <math.h>
using namespace std;
int n;
// 最优写法
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ ) {
if (x % i == 0) return false;
}
return true;
}
// 借助sqrt函数
bool is_prime2(int x) {
if (x < 2) return false;
int q = sqrt(x);
for (int i = 2; i <= q; i ++ ) {
if (x % i == 0) return false;
}
return true;
}
// 不推荐,当i过大时,i*i会溢出变为负数,导致结果出错
bool is_prime3(int x) {
if (x < 2) return false;
for (int i = 2; i * i <= x; i ++ ) {
if (x % i == 0) return false;
}
return true;
}
int main() {
scanf("%d", &n);
while (n -- ) {
int x;
scanf("%d", &x);
if (is_prime(x)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
分解质因数
对于一个大于2的正整数,可以被分解为质数相乘的形式,比如100=2* 2* 5* 5,2和5也叫做质因数。想要获取质因数,依旧可以穷举,当n可以被2整除就一直对它除2(期间记录可以除多少个2),当n可以被3整除就一直对它除3...直到穷举到sqrt(n)。
该方法的正确性源于,使用2除尽后,n就不可能再被诸如4、8这类数整除,以此类推。该算法时间复杂度介于 O(logn)-O(sqrt(n))
例题:https://www.acwing.com/problem/content/869/
#include <iostream>
using namespace std;
int n;
void func(int x) {
for (int i = 2; i <= x / i; i ++ ) {
int cnt = 0;
while (x % i == 0) {
x /= i;
cnt ++ ;
}
if (cnt) printf("%d %d\n", i, cnt);
}
// 在最后会由于 i <= x / i 导致剩余一个质数
if (x != 1) printf("%d %d\n", x, 1);
printf("\n");
}
int main() {
scanf("%d", &n);
while (n -- ) {
int x;
scanf("%d", &x);
func(x);
}
return 0;
}
筛质数
筛质数就是将1-n中的质数筛选出来。
例题:https://www.acwing.com/problem/content/870/
埃氏筛
当一个数为质数时,将由该质数组成的合数筛掉,例如2是质数,那么4、6、8、10...都是以2为质因数的合数,因此他们都将被筛掉。之后的3作为质数也重复这一操作...埃氏筛时间复杂度为O(nloglogn)
#include <iostream>
using namespace std;
const int N = 1000010;
int prime[N];
bool st[N];
void ai_screen(int x) {
int cnt = 0;
for (int i = 2; i <= x; i ++ ) {
// i是质数,将以i为因子的合数筛掉
if (!st[i]) {
// 记录质数
prime[cnt ++ ] = i;
// 筛掉合数
for (int j = i; j <= x; j += i) {
st[j] = true;
}
}
}
printf("%d", cnt);
}
int main() {
int n;
scanf("%d", &n);
ai_screen(n);
return 0;
}
问题主要在于,如何保证遍历过程中,遇到的数一定是质数?
做不严谨的个证明:假设遇到数为p,那么如果p为合数,一定会被从2到p-1的质数筛选掉(从2到p-1一定存在p的质因子),因此p一定是质数。
线性筛
在埃氏筛中可以发现,一个数会被重复的筛掉,例如6会被2和3重复筛,12会被2、3、4重复筛... 因此优化的地方在于,我们期望一个合数只会被它的最小质因子筛掉,即12被2筛过后,就不会再被3、4、6筛选。
换句话说就是在遇到2的时候,只将以2作为最小质因数的合数筛掉,例如2、4、6、8、10、12...遇到3的时候只将以3作为最小质因数的合数筛掉,例如9、15...
#include <iostream>
using namespace std;
const int N = 1000010;
int prime[N];
bool st[N];
void linear_screen(int x) {
int cnt = 0;
for (int i = 2; i <= x; i ++ ) {
if (!st[i]) prime[cnt ++ ] = i;
// 从小到大枚举已知的质数
for (int j = 0; prime[j] <= x / i; j ++ ) {
// prime[j]是i的最小质因数,
// 因此prime[j]也一定是prime[j] * i的最小质因数
if (i % prime[j] == 0) {
st[prime[j] * i] = true;
break;
}
// prime[j]不是i的质因数,但是prime[j]一定小于i的最小质因数,
// 因此prime[j]也一定是prime[j] * i的最小质因数
else {
st[prime[j] * i] = true;
}
}
}
printf("%d", cnt);
}
int main() {
int n;
scanf("%d", &n);
linear_screen(n);
return 0;
}
举例来说明算法流程:
1、从2开始枚举,将2作为质数存储,2%2=0,因此4将被筛掉
2、枚举到3,3作为质数存储,3%2!=0,因此6倍筛掉;3%3=0,因此9被筛掉
3、枚举到4,4不是质数,4%2=0,因此8被筛掉
4、枚举到5,5作为质数存储,5%2!=0,因此10被筛掉;5%3!=0,因此15被筛掉
5、枚举到6,6不是质数,6%2=0,因此12被筛掉
6、...
往后列举可以发现,每个合数都是被它的最小质因子筛掉的。i在枚举过程中,不断的和已确定的质数相乘,筛掉后面未枚举到的合数。已确定的质数也是按从小到大的顺序排列的,因此可以确保使用最小质因子筛选合数。
当i可以被某个质因子x整除时,i不能再与比x大的质因子相乘进行筛选,因为这过多的操作会在i枚举到其他数时出现重复。比如i=6,在与2相乘后就不再继续和其他质因子相乘。如果我们硬要相乘,那么6* 3=18将被筛掉。在i=9时,将会重复的执行9* 2=18,18被筛了两次。
将上文代码精简,可以得到如下代码:
void linear_screen(int x) {
int cnt = 0;
for (int i = 2; i <= x; i ++ ) {
if (!st[i]) prime[cnt ++ ] = i;
// 从小到大枚举已知的质数
for (int j = 0; prime[j] <= x / i; j ++ ) {
// prime[j]是i的最小质因数,
// 因此prime[j]也一定是prime[j] * i的最小质因数
// prime[j]不是i的质因数,但是prime[j]一定小于i的最小质因数,
// 因此prime[j]也一定是prime[j] * i的最小质因数
st[prime[j] * i] = true;
if (i % prime[j] == 0) break;
}
}
printf("%d", cnt);
}
对于循环退出条件prime[j] <= x / i
可以变换为prime[j] * i <= x
,目的是防止相乘后要筛掉的值大于n,防止做无用筛选。由乘法换为除法是为了防止相乘导致溢出。
另外,j不需要判断越界(j<cnt),原因有2:
1、当i为质数,i会被计入prime数组,随着遍历prime数组,i到最后会和自己整除,进而退出循环
2、当i为合数,i在遍历prime数组过程中,就将会被某个质数作为因子整除,进而退出循环