文章目录
求素数
素数是数学的一大分支,也是编程中数学解题的一大基础。因此,求素数是一项基础知识。
1.暴力
这是最没用的算法,时间复杂度太高:枚举 n n n以内的每一个数,再用试除法判断该数是否为素数。
//Maxn 为数组q的大小。
bool check(int n)
{
for(int i = 0 ; i * i < n ; i ++)
if(n % i == 0)
return false;
return true;
}//试除法
void make_q(int n)
{
for(int i = 2 ;i <= n ; i++)//1和0既不是质数也不是合数,应单独判断
if(check(i))
q[i] = true
}
实际上该算法只需要知道有这种算法就行了,一般不会将试除法用于除判断素数之外的地方
2. E r a t o s t h e n e s Eratosthenes Eratosthenes筛法
最常用的素数筛,其原理是每枚举到一个素数就将其倍数全标记为合数,那么没有统计到的就是素数。计算到 n \sqrt{n} n时就可以筛出小于 n n n的所有素数。
code
bool q[Size];//Size为n的最大值。
void make_q(int n)
{
q[1] = false;
q[0] = false;
for(int i = 2 ; i * i <= n ; i++)//初始化。
q = true;
for(int i = 2 ; i * i <= n ; i++)
{
for(int j = i * i ; j <= n ; j += j)
{
q[j] = false;
}
}
}
但在这种情况下还是会有大量点被重复标记,所以可以选择只筛素数的倍数,这与筛所有数的倍数是等效的(参照算术基本定理)。
bool q[Size];
void make_q(int n)
{
q[0] = false;
q[1] = false;
for(int i = 2 ; i * i <= n ; i++)
{
if(q[i])
{
for(int j = i * i ; j < n ; j++)
{
q[j] = true;
}
}
}
}
复杂度
O
(
n
×
log
(
log
n
)
)
O(n\times\log{(\log{n})})
O(n×log(logn))
这是算法竞赛中最常用的一个求素数算法,因为它好写,且复杂度实际上接近
O
(
n
)
O(n)
O(n),所以十分好用。
3.线性筛法(欧拉筛)
埃氏筛虽然复杂度接近线性但还是有一定偏差,所以欧拉筛应运而生 欧拉筛是一种线性复杂度的素数筛算法,其原理基础是算术基本定理。
基于算术基本定理,每一个数一定都可以表示为若干个质数之积(质数除外)。所以每一个数的最小质因子是确定的。因此我们可以用最小质因子来标记每一个数,对于每一个数 i i i而言,对于每一个小于 i i i的最小质因子的质数 j j j都可以筛掉 i × j i\times j i×j这个数。且对于不同的 i i i而言满足 i × j i\times j i×j不重不漏。
举例:一开始枚举 2 2 2时将 2 2 2标记为质数并将其最小质因数定为 2 2 2,然后将 2 × 2 2\times2 2×2的最小质因数标记为 2 2 2,接下来枚举 3 3 3时重复上述操作但当标记时是先将小于 3 3 3的 2 2 2与之相乘得到的 6 6 6的最小质因子标记为 2 2 2然后再算 3 × 3 3\times3 3×3的最小质因子。像这样每次将小于等于该数的最小质因子的数与这个数本身相乘,就能做到不重不漏。
具体看代码
code
int f[Size], q[Size];
void make_q(int n)
{
int top = 0;
for(int i = 2 ; i <= n ; i++)
{
if(!f[i])
{
f[i] = i;
q[top++] = i;
}
for(int j = 0 ; p[j] <= f[i] && j < top ; j++)
{
f[i * p[j]] = p[j];
if(p[j] > n / i)//当所枚举到的质数与当前数的积大于n是退出。
break;
}
}
}
复杂度
O
(
n
)
O(n)
O(n)。
这是一个竞赛上常用的求素数算法,因为较
E
r
a
t
o
s
t
h
e
n
e
s
Eratosthenes
Eratosthenes筛法而言优化到了彻底的
O
(
n
)
O(n)
O(n)。所以可以解决一些
E
r
a
t
o
s
t
h
e
n
e
s
Eratosthenes
Eratosthenes筛法无法解决的问题。
质因子分解
对于不同的题,有时候将某一个数有规律地用其他的数表示出来会更易于解题或者优化,而基于算术基本定理的质因子分解就是最方便的方法之一。
基础算法
依据算术基本定理,每一个数都可以分解成有限的质数的成积。因此,我们可以将一个数唯一地分解为若干个质数之积,这若干个质数就是该数的质因子分解结果。
由于每个数 n n n可以被唯一的一组质数表示,所以可以从小到大枚举该数的因子 i i i,对每一个因子, 都将 n n n除去这个因数 i i i,直到 i i i不再是当前 n n n的因子,因为合数可以表示为若干个质数之积,所以每一个去除的 i i i一定是该数的质因子。
int qn[Size][2]//Size是该数的质因子个数范围,第二维限制储存质因子和它的数目。
void devide(int n)
{
int tot = 0;
for(int i = 2 ; i <= n ; i++)//因为n可能本身就是素数所以i枚举到n.
{
if(n % i == 0)
qn[tot][0] = i;
while(n % i == 0)
{
n /= i;
qn[tot][1]++;
}
}
}
复杂度最差 O ( n ) O(n) O(n)
优化1
由于任何一个数 n n n,它的的质因子要么是他本身,要么不大于 n \sqrt{n} n,所以循环时只需要查找 1 1 1~ n \sqrt{n} n是否为 n n n的约数即可。
优化后得到下列代码:
int qn[Size][2]//Size是该数的质因子个数范围,第二维限制储存质因子和它的数目。
void devide(int n)
{
int tot = 0;
int temp = sqrt(n);//sqrt()来自#include<cmath>库
for(int i = 2 ; i <= temp ; i++)
{
if(n % i == 0)
qn[tot][0] = i;
while(n % i == 0)
{
n /= i;
qn[tot][1]++;
}
}
if(n > 1)//处理n是质数的情况
{
qn[tot][0] = n;
qn[tot][1] = 1;
}
}
优化2
此时的复杂度已经降到了 O ( n ) O(\sqrt n) O(n)。但还是有多余的扫描:我们只需要查找质因子即可。所以可以先用素数筛求出小于等于 n \sqrt{n} n的素数,再分解质因子,如下。
int f[Size], q[Size] , qn[Size][2];
int top = 0;
void make_q(int n)
{
for(int i = 2 ; i <= n ; i++)
{
if(!f[i])
{
f[i] = i;
q[top++] = i;
}
for(int j = 0 ; p[j] <= f[i] && j < top ; j++)
{
f[i * p[j]] = p[j];
if(p[j] > n / i)
break;
}
}
}//这里以线性筛为例。
void devide(int n)
{
int tot = 0;
int temp = sqrt(n);//sqrt()来自#include<cmath>库
for(int i = 0; i < top ; i++)
{
if(n % q[i] == 0)
qn[tot][0] = i;
while(n % q[i] == 0)
{
n /= q[i];
qn[tot][1]++;
}
}
if(n > 1)//处理n是质数的情况
{
qn[tot][0] = n;
qn[tot][1] = 1;
}
}