质数、分解质因数、质数筛

1. 质数的定义

质数:在大于1的整数中,如果某一个数仅仅包含1和其本身这两个约数,则称这个数为质数,或者叫素数

2. 质数的判断—试除法

首先,有一种从其定义出发的判定方法,时间复杂度是 O ( n ) O(n) O(n)

bool is_prime(int x)
{
	if (x < 2) return false;
	for (int i = 2; i < x; i ++ )
		if (x % i == 0)
			return false;
	return true;
}

但是,我们可以发现这么一个性质:如果 d d d x x x 的约数,记为 d ∣ x d|x dx,那么 x d \frac{x}{d} dx 也一定是 x x x 的约数,因此在如果遇到了 x x x 的约数,那么 d d d x d \frac{x}{d} dx 一定是成对出现的,因此我们始终可以枚举其中较小的那一个,即 d ≤ x d d ≤ \frac{x}{d} ddx,也就是 d 2 ≤ n d^2≤n d2n,这样就可以将时间复杂度降至 O ( s q r t ( n ) ) O(sqrt(n)) O(sqrt(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;
}

3. 分解质因数—试除法

3.1 概念

质因数就是一个数的约数,并且该约数为质数

比如 8 = 2 × 2 × 2 8=2×2×2 8=2×2×2,那么 2 2 2 就是 8 8 8 的质因数。

比如 12 = 2 × 2 × 3 12 = 2×2×3 12=2×2×3,那么 2 2 2 3 3 3 就是 12 12 12 的质因数。

把一个数字以 12 = 2 × 2 × 3 12 = 2×2×3 12=2×2×3 的形式表示,叫做分解质因数,只有合数才能分解质因数。

3.2 方法

假设求 x x x 的质因数,那么就从小到大枚举 x x x 的所有约数,如果 d d d x x x 的一个约数,则继续计算其次数。(当我们从小到大枚举到 d d d 的时候,就已经将 x x x 2 2 2 ~ d − 1 d-1 d1 中所有的质因子都除干净了,同理 d d d 也一定不包括 2 2 2 ~ d − 1 d-1 d1 的质因子,那么这个约数 d d d 一定是一个质数)

最直白的方法如下,时间复杂度为 O ( n ) O(n) O(n)

void divide(int x)
{
	for (int i = 2; i <= x; i ++ )
		if (x % i == 0) // i 一定是质数
		{
			int s = 0;
			while (x % i == 0)
			{
				x /= i;
				s ++ ;
			}
			printf("%d %d\n", i, s);
		}
	puts("");
}

但是如何优化上述代码的时间复杂度呢?这里介绍一个性质:

x x x最多只包含一个大于 s q r t ( x ) sqrt(x) sqrt(x)质因子。因此,可以先将所有小于 s q r t ( x ) sqrt(x) sqrt(x) 的质因子先枚举出来,如果最后 x > 1 x > 1 x>1 则将其单独输出出来,因此这样可以有效的将求质因子的时间复杂度降低到最差 O ( s q r t ( n ) ) O(sqrt(n)) O(sqrt(n))

注意:假如说一个数为 x = 2 k x=2^k x=2k,则只需要 l o g n logn logn 次操作就可以结束该算法,因此求质因数的算法时间复杂度介于 O ( l o g n ) O(logn) O(logn) ~ O ( s q r t ( n ) ) O(sqrt(n)) O(sqrt(n)) 之间。

void divide(int x)
{
	for (int i = 2; i <= x / i; i ++ )
		if (x % i == 0)
		{
			int s = 0;
			while (x % i == 0)
			{
				x /= i;
				s ++ ;
			}
			printf("%d %d\n", i, s);
		}
	if (x > 1) printf("%d %d\n", x, 1);
	puts("");
}

4. 质数筛

4.1 埃式质数筛

假如我们现在要计算小于等于 x x x 的所有质数,则我们列一个 [ 2 , 3 , 4 , . . , x − 1 , x ] [2,3,4,..,x-1,x] [2,3,4,..,x1,x] 的数表,从 2 2 2 开始从左往右枚举,假设当前枚举到 2 2 2,则将所有 ≤ x ≤x x 2 2 2 的倍数全部标记为非质数,所有非质数枚举到的时候直接跳过,那么假设下一次枚举到 d d d d d d 没有被标记为非指数,则说明 d d d 无法被 [ 2 , 3 , . . . , d − 1 ] [2,3,...,d-1] [2,3,...,d1] 的数整除,因此 d d d 也就是质数。

这种时间复杂度计算为 x 2 + x 3 + x 4 + . . . + x x = x ( 1 2 + 1 3 + . . . + 1 x ) = x ( l n x + c ) \frac{x}{2} + \frac{x}{3} + \frac{x}{4} + ... + \frac{x}{x}=x(\frac{1}{2} + \frac{1}{3} + ... + \frac{1}{x})=x(lnx + c) 2x+3x+4x+...+xx=x(21+31+...+x1)=x(lnx+c)。因此,整个算法的时间复杂度可以近似为 O ( n l o g n ) O(nlogn) O(nlogn)

测试:在 1 e 6 1e6 1e6 的测试数据下运行了 146 m s 146ms 146ms

#include <iostream>
#include <cstring>
const int N = 1e6 + 10;

int primes[N], cnt;
bool st[N];

void get_primes(int x)
{
	for (int i = 2; i <= x; i ++ )
	{
		if (!st[i]) primes[cnt ++ ] = i;
		for (int j = i + i; j <= x; j += i) st[j] = true;
	}
}

但是这里可以做一个优化,也就是在做筛掉倍数的时候并不需要将每一个数的倍数全部筛掉,而仅仅只需要将 2 2 2 ~ x − 1 x - 1 x1 中的所有质数的倍数筛掉就可以了,当一个数不是质数的时候我们就不需要筛掉其所有倍数,因此我们可以将循环放入判断中去,代码如下。

#include <iostream>
#include <cstring>
const int N = 1e6 + 10;

int primes[N], cnt;
bool st[N];

void get_primes(int x)
{
	for (int i = 2; i <= x; i ++ )
	{
		if (!st[i])
		{
			primes[cnt ++ ] = i;
			for (int j = i + i; j <= x; j += i) st[j] = true;
		}
	}
}

此时,我们再来分析一下时间复杂度:

原来的时间复杂度是这样计算的: x ( 1 2 + 1 3 + . . . + 1 x ) = x ( l n x + c ) x(\frac{1}{2} + \frac{1}{3} + ... + \frac{1}{x})=x(lnx + c) x(21+31+...+x1)=x(lnx+c),但是由于质数定理: [ 1 , 2 , . . . , x ] [1,2,...,x] [1,2,...,x] 中最多只有 x l n x \frac{x}{lnx} lnxx 个质数,因此优化后的时间复杂度应改为 x ′ ( l n x ′ + c ) = x l n x ( l n x ′ + c ) ≈ O ( x ) x'(lnx' + c) = \frac{x}{lnx}(lnx' + c)≈O(x) x(lnx+c)=lnxx(lnx+c)O(x) ,但是这样计算并不准确(实际为 O ( n l o g l o g n ) O(nloglogn) O(nloglogn) ),只能说大致接近于 O ( n ) O(n) O(n)

测试:在 1 e 6 1e^6 1e6 的测试数据下运行了 54 m s 54ms 54ms,快了 3 3 3 倍左右。

4.2 线性质数筛(推荐)

事先说明:线性筛法在 1 e 6 1e^6 1e6 的数据规模下和埃式筛法效率差不多,但是在 1 e 7 1e^7 1e7 的数据规模下线性筛法能比埃式筛法快一倍。因此,我们更推荐线性筛法,因为其使用范围更广。

原理如下:每一个数 i i i 只会被它的最小质因子筛掉

  1. i % p r i m e s [ j ] = = 0 i \% primes[j] == 0 i%primes[j]==0 时, p r i m e s [ j ] primes[j] primes[j] 一定是 i i i 的最小质因子, p r i m e s [ j ] primes[j] primes[j] 也一定是 p r i m e s [ j ] ∗ i primes[j] * i primes[j]i 的最小质因子。
  2. i % p r i m e s [ j ] ! = 0 i \% primes[j] != 0 i%primes[j]!=0 时,由于我们是从小到大枚举的质数,并且我们没有枚举到 i i i 的任何一个质因子,说明 p r i m e s [ j ] primes[j] primes[j] 一定是小于 i i i 的所有质因子,因此 p r i m e s [ j ] primes[j] primes[j] 也一定是 p r i m e s [ j ] ∗ i primes[j] * i primes[j]i 的最小质因子。

因此,我们筛的每一个数,我们一定是去用它的最小质因子去筛它。

对于一个合数 x x x,假设 p r i m e s [ j ] primes[j] primes[j] x x x 的最小质因子,当 i i i 枚举到 x p r i m e s [ j ] \frac{x}{primes[j]} primes[j]x 的时候一定会被筛掉,并且一定是被其最小质因子筛掉。而每个数的最小质因子只有一个,因此每个合数只会被筛掉一次,所以是线性的。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e7 + 10;
int primes[N], cnt;
bool st[N];

void get_primes(int x)
{
	for (int i = 2; i <= x; i ++ )
	{
		if (!st[i]) primes[cnt ++ ] = i;
		for (int j = 0; primes[j] <= x / i; j ++ ) // 从小到大枚举所有的质数
		{
			st[primes[j] * i] = true;	   
			if (i % primes[j] == 0) break; // 因此,第一次满足这个条件时,primes[j] 一定是 i 的最小质因子
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁头娃撞碎南墙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值