水了一篇啊...
素数判断法
朴素判断
众所周知,大于等于(2)的仅含有(1)和自身这两个因子的正整数被称作素数
故只要判断在([2,n-1])范围内是否存在其它因子,就可以判断(n)是否为素数了
无脑判断法直接从(2)for到(n-1),稍微优化下可以缩减到([2,frac n 2]),但这个方法时间复杂度为(O(n))
可以发现,如果某个数(x)是(n)的因子,那么(frac n x)也一定是(n)的因子(可能(x==frac n x))
所以如果将这两个因子看作一组,我们其实只需要判断([2,sqrt n])以内是否有素数即可,时间复杂度为(O(sqrt n))
#include<cmath>
bool isprime(int n)
{
int d=sqrt(n);
for(int i=2;i<=d;i++)
if(n%i==0)
return false;
return true;
}
bool isprime2(int n) //写着比较方便,下面都用这种写法
{
for(int i=2;i*i<=n;i++)
if(n%i==0)
return false;
return true;
}
六除法判断
根据素数((≥5))的分布性质可以得知
每个素数都与(6)的倍数相邻
例如(5,7 ; 11,13 ; 17,19...)
所以得出的结论是,如果一个(≥5)的数对(6)取模不等于(1)或者(5),那么这个数一定不是素数
于是我们就能从(5)开始,以(6)为步数判断到(sqrt n)过,每次只需要判断(i)与(i+2)是否为因子,大大提升了判断效率
时间复杂度小于(O(sqrt n))
bool isprime(int n)
{
if(n<=3) //特判2,3
return n>1;
if(n%6!=1&&n%6!=5)
return false;
for(int i=5;i*i<=n;i+=6)
if(n%i==0||n%(i+2)==0)
return false;
return true;
}
筛法判断素数
下文将会阐述几种筛法
通过(O(n))、(O(nloglogn))几种不同时间复杂度筛出素数后实现(O(1))查询
但将会受限于空间
Miller-Rabin素数检测
这是对于一个不能在(O(sqrt n))时间范围内判断出是否为素数的大数的测试方法
通过选取小素数去测试被测数是否为素数
但非素数也有(frac 1 4)的概率会通过检测
所以需要多次选取不同素数进行测试,若都通过则大概率为素数
注:如果(int)范围内的某个被测数用(30)以内的素数均能通过测试,可以得到该数为素数。
引理 1 ——费马小定律:
设$p$是素数,$a$为整数,且$(a,p)=1$,则$a^{p-1}≡1 (mod p)$
引理 2 ——二次探测定理:
如果$p$是一个素数,且$0<x<p$,则方程$x^2≡1 (mod p)$的解为$x_1=1,x_2=p-1$
所以该算法,流程如下
-
设(s,t)使得(2^s*t=x-1)((t\%2==1))
-
取一小素数(a),算出(a^t),然后不断平方并进行二次检测(进行(s)次)
-
根据费马小定律,如果(a^{x-1} !≡ 1 (mod p)),则可得(x)非素数
-
多次进行检测,可使得正确性概率更高
typedef long long ll;
int prim[20]={2,3,5,7,11,13,17,19,23,29};
ll qmul(ll a,ll b,ll mod) //快速乘
{
ll r=0;
while(b)
{
if(b&1)
r=(r+a)%mod;
a=(a+a)%mod;
b>>=1;
}
return r;
}
ll qpow(ll a,ll n,ll mod) //快速幂
{
ll r=1;
while(n)
{
if(n&1)
r=(r*a)%mod;
n>>=1;
a=(a*a)%mod;
}
return r;
}
bool Miller_Rabin(ll x)
{
int s=0;
ll t=x-1;
if(x<=3)
return x>1;
if(!(x&1))
return false; //特判一些基本情况
while(!(t&1)) //将x-1分解成(2^s)*t
{
s++;
t>>=1;
}
for(int i=0;i<10&&prim[i]<x;i++)
{
ll b=qpow(prim[i],t,x);
for(int j=1;j<=s;j++) //s次平方
{
ll k=qmul(b,b,x);
if(k==1&&b!=1&&b!=x-1) //用二次探测判断
return false;
b=k;
}
if(b!=1)
return false; //用费马小定律判断
}
return true;
}
该检测方法常运用于大数因数分解Pollard rho算法中
素数筛法
朴素筛法
基于朴素判断法的朴素筛法
复杂度(O(n^{frac 3 2})),很慢
bool prime[maxn];
bool isprime(int n)
{
for(int i=2;i*i<=n;i++)
if(n%i==0)
return false;
return true;
}
void primeSelect()
{
prime[0]=prime[1]=false;
for(int i=2;i<=maxn;i++)
prime[i]=isprime(i);
}
Eratosthenes筛法/埃氏筛
埃氏筛法是一种从(2)开始,通过质因子枚举的方式筛去所有合数,使得最后留下的均为素数
如果数字(x)是素数,那么所有以(x)为因子的数均非素数(素数性质)
所以接下来可以枚举以(x)为因子的数并标记为合数即可
优化1:每次找到一个素数(x),枚举下界可以是(x^2)而非(2x)(可证得),可小幅度缩短筛选时间
优化2:(根据优化1可得)素数枚举只要求到(sqrt n)即可筛出(1)到(n)内的所有素数
时间复杂度(O(nloglogn)),较为高效(还好写)
bool prime[maxn];
void primeSelect(int n)
{
memset(prim,true,sizeof prim);
prim[0]=prim[1]=false;
for(int i=2;i*i<=n;i++) //筛到sqrt n即可
if(prim[i])
for(int j=i*i;j<=n;j+=i) //以i*i为下界
prim[j]=false;
}
如需素数记录,外层循环改为(2)到(n)即可
int prime[maxn],cnt=0;
bool vis[maxn];
void primeSelect(int n)
{
for(int i=2;i<=n;i++)
if(!vis[i])
{
prime[++cnt]=i;
for(int j=i*i;j<=n;j+=i)
vis[j]=false;
}
}
欧拉筛
可以发现,埃氏筛法虽然较为高效,但未经判断直接开始枚举质因子的倍数则会产生一定数量的重复标记
重复对某些已经筛出的合数进行标记,只会浪费复杂度,这也正是其(O(nloglogn))的由来(在一定程度上,(loglogn)已经非常高效)
但我们不满足于这种看似高效的算法,而需要真正意义上的线性筛
所以在埃氏筛法的基础上,让每个合数只被它的最小质因子筛选到一次即判断退出,以达到不重复的目的
这便是欧拉筛法,时间复杂度(O(n))
int prime[maxn],cnt=0;
bool vis[maxn];
void primeSelect()
{
memset(vis,false,sizeof vis);
for(int i=2;i<=maxn;i++)
{
if(!vis[i]) //没访问过的就是素数
prime[++cnt]=i;
for(int j=1;j<=cnt&&i*prime[j]<=maxn;j++) //遍历每一个已找到的素数
{
vis[i*prime[j]]=true;
if (i%prime[j]==0) //每个数只被它的最小质因子筛一次
break;
}
}
}
参考文章:
《Miller-Rabin素数测试算法》 - forever_dreams