利用费马小定理进行素性测试
Description
给出一个整数N,请利用费马小定理(Fermat’s Little Theorem)测试该数是否素数。
Input
多测试用例。每个测试用一行:一个正整数N ( 3 ≤ N ≤ 9223372036854775807 ,N的范围就是 long long 的范围。注意:本OJ不支持__int64这种类型,所以,如果要用 __int64类型,可直接把它改为 long long类型。__int64的输入输出用 %I64d ,long long的输入输出用%lld )
Output
每个测试用例输出一行结果:如果N是素数,输出yes,否则,输出no 。
Sample Input
127
5
67
68
Sample Output
yes
yes
yes
no
一开始的思路,只用了费马小定理的代码,递归次数太多了,如果数太大需要耗费很长时间才能计算出结果。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//费马小定理
long long fermat(long long a,long long n,long long m)
{
//递归出口
if(m==0)return 1;
else if(m==1)
return a;
//递归
else{
if(m%2==0)
return (fermat(a,n,m/2)%n*fermat(a,n,m/2)%n)%n;
else
return ((fermat(a,n,m/2)%n*fermat(a,n,m/2)%n)%n*a)%n;
}
}
int main(void)
{
long long n,a=0;
scanf("%lld",&n);
while(n>=3&&n<=9223372036854775807)
{
srand((unsigned)time(NULL));
//生成一个足够大的随机数(64位)
for(int i=0;i<4;i++)
{
a=a+rand();
a=a<<16;
}
//求n的模
a=a%n;
if(a<2)a=2;
if(fermat(a,n,n-1)==1)
printf("yes\n");
else
printf("no\n");
scanf("%lld",&n);
}
return 0;
}
对于这道题,如果单纯只用费马小定理的话来做这道算法的话呢,理论是可以的,但是时间复杂度太高,计算一个大的数时需要很长时间,所以出现了Time Limit Exceeded。所以不得不再去研究如何去设计一种可以快速得出结果的算法。(研究了好几天还是没有头绪,最后还是去请教了一位已经AC过这道题的师兄-.-)
师兄给出了做这道题的思路,对于这道题他使用一种高效又准确的算法,通过整合费马小定理+高精度算法+高精度求余+快速幂这几个算法来实现。首先我们先来了解一下什么是费马小定理、高精度算法、高精度求余和快速幂,这几个算法也挺容易理解的。
- 费马小定理
费马小定理(Fermat’s little theorem)是数论中的一个重要定理,在1636年提出,其内容为: 假如p是质数,且gcd(a,p)=1,那么 a(p-1)≡1(mod p),即:假如a是整数,p是质数,且a,p互质(即两者只有一个公约数1),那么a的(p-1)次方除以p的余数恒等于1。
注意:可以用费马小定理判断素数,但有一定概率是错误的。费马小定理只是素数的必要条件,不是充分条件,即:满足费马小定理的数不一定是素数。
虽然有一定的概率出错,但效率高,而且也可以通过使用随机数生成a 或者增加验证费马小定理的次数来减小出错率。
- 高精度乘法
计算机内部直接使用int和double等数据类型储存数字是有范围限制的,如:C语言的int类型与开发环境平台有关,可能是16位(2^16),也可能是32位(2^32)。当数据运算大小超过范围后,计算机将会出现溢出情况,使得计算结果不准确。当我们要计算高位数的运算时,我们可以选择高精度乘法。
高精度乘法原理:
原理是用数组或者字符串来存取数据,模拟手算进行每一位的相乘,将相乘结果按位相加,整合成一个运算结果。
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
typedef unsigned long long ULL;
/*
*高精度乘法,原理同平时手算 0000
* *0000
*ULL a 乘数a
*ULL b 乘数b
*char numbera[] 用字符数组接收a
*char nmuberb[] 用字符数组接收b
*
*char c[] 接受结果
*
*/
void BigIntMult(ULL a,ULL b)
{
int numbera[1500];
int numberb[1500];
int c[3000];
// printf("%d",sizeof(numbera)/sizeof(numbera[0]));
// numbera=toInts(a);
// numberb=toInts(b);
int i=0;
//把a转化为int类型的数组
while(a>0)
{
numbera[i]=a%10;
a/=10;
i++;
// printf("a\n");
}
i=0;
//同上
while(b>0)
{
numberb[i]=b%10;
b/=10;
i++;
// printf("b\n");
}
// printf("60\n");
//sizeof(c)/sizeof(c[0]) --> 这个是求出数组的长度
for(int s=0;s<sizeof(c)/sizeof(c[0]);s++)
c[s]=0;
// printf("63\n");
//相乘
int j;
for(i=0;i<sizeof(numbera)/sizeof(numbera[0]);i++)
{
for(j=0;j<sizeof(numberb)/sizeof(numberb[0]);j++)
{
c[i+j]+=numbera[i]*numberb[j];
}
}
// printf("73\n");
//进位
for(i=0;i<sizeof(c)/sizeof(c[0]);i++)
{
if(c[i]>=10)
{
c[i+1]+=c[i]/10;
c[i]%=10;
}
}
// printf("83\n");
i=sizeof(c)/sizeof(c[0]);
//找出有效位的最高位
while(i>0&&c[--i]==0);
//从最高位开始向后输出
for(j=i;j>=0;j--)
{
printf("%d",c[j]);
}
printf("\n");
}
int main(void)
{
ULL b,c;
long long a;
// printf("hahah");
// scanf("%d",&c);
// printf("%d\n",c);
// while(scanf("%lld %lld",&a,&b)!=EOF)
scanf("%lld",&a);
scanf("%lld",&b);
BigIntMult(a,b);
return 0;
}
- 高精度求余
原理同高精度乘法(除法),用数组或字符串存取数据,模拟手算求模,从高位往下计算,a[n]%mod=res,下一位则用(res*10+a[n-1])%mod求出模,同之前的操作以此类推下去直到n=0,最后结果就是要求的余。
#include<stdlib.h>
#include<stdio.h>
void BigIntMod(long long num,long long mod)
{
int a[1500];
long long res=0;
int i=0;
//初始化a
for(i=0;i<sizeof(a)/sizeof(a[0]);i++)
{
a[i]=0;
}
//将num转化为数组
i=0;
while(num>0)
{
a[i++]=num%10;
num/=10;
}
//求数组范围
i=sizeof(a)/sizeof(a[0]);
//取出有效值的起始下标
while(i>=0&&a[--i]==0);
int j=i;
for(i=j;i>=0;i--){
printf("%d",a[i]);
}
printf("\n16\n");
//有效位逐一进行求模
for(i=j;i>=0;i--)
{
res=(res*10+a[i])%mod;
}
printf("%lld",res);
}
int main()
{
long long a,b;
scanf("%lld%lld",&a,&b);
BigIntMod(a,b);
return 0;
}
- 快速幂
其实快速幂很容易理解,就是利用递归,将幂逐步拆分,如a^n : 当n为偶数时,每一次将n/2,便成了(a^(n-1)*a^(n-1)),当n为奇数时,n/2会向下取整,从而当将n除以2的时候,要注意此时会丢失a^1,变成了(a^(n-1)*a^(n-1)),这时候我们要用一个变量将丢失的存储起来,最后再同后边的结果相乘起来。
#include <stdio.h>
#include<stdlib.h>
LL quickpow(long long num,long long power)
{
LL n=num,res=1;
while(power)
{
//如果power是奇数,power/2会丢失 n^1,这一步用来把n保存下来
if(power&1)
res=res*n;
n=n*n;
power>>=1;
}
return res;
}
int main(void)
{
long long num,power;
scanf("%lld%lld",&num,&power);
long long result=quickpow(num,power);
printf("%lld",result);
return 0;
}
最后整合起来的代码是这个样子的,可是最终结果还是Wrong Answer,思路是没问题的,只是可能我哪个地方出错了,没有找出来(可能是我太菜了- -)(这里就不贴代码了,跟下面的比较只是这里的数据类型我定义为long long)
最后试了各种方法终于找到问题所在:溢出。出乎意料问题竟然是这个,之前一直被高精度乘法跟高精度求不会溢出一直没有从这个方向去找,后来通过BNUEPOfflineJudge(离线OJ)找出了一个测试数据的输出结果错误,找到这个测试数据然后再单步调试,发现了程序中一个方法中的res(余)竟然是负数,慢慢顺着线索追上去再去查看了一下代码,发现了一行不起眼的代码,嗯就是这行
res=(res*10+c[i])%mod;
就是这里*10后溢出了。后面把long long 的数据类型都改为unsigned long long 后终于没问题。
(下面是最后AC的代码)
#include<stdlib.h>
#include<stdio.h>
#include<math.h>
#include<time.h>
#define TIMES 10
//926911036283044987
/*
*费马小定理+快速幂+高精度乘法+高精度求模
*
*
*
*
*/
//函数声明
unsigned long long quickpow(unsigned long long num,unsigned long long power,unsigned long long mod);
unsigned long long bigIntMult(unsigned long long a,unsigned long long b,unsigned long long mod);
void fermat(long long num);
//将num转换成int类型的数组
long long toString(int str[],unsigned long long num,int len)
{
int i;
for(i=0;i<len;i++)
str[i]=0;
i=0;
while(num>0)
{
str[i++]=num%10;
num/=10;
}
//返回位数
return i;
}
//费马小定理
void fermat(unsigned long long num)
{
// if(num==1||num==2)
// {
// printf("yes\n");
// return;
// }
//验证次数:最高TIMES次
int times=(sqrt(num)>TIMES)?TIMES:sqrt(num);
unsigned long long r;
// srand((unsigned)time(NULL));
while(times--)
{
r=rand()%num;
if(r==0||r==1)r=2;
if(quickpow(r,num-1,num)!=1)
{
printf("no\n");
return;
}
}
printf("yes\n");
}
//快速幂
unsigned long long quickpow(unsigned long long num,unsigned long long power,unsigned long long mod)
{
unsigned long long n=num,res=1;
while(power)
{
res%=mod;
if(power&1)
res=bigIntMult(res,n,mod);
n=bigIntMult(n,n,mod);
power>>=1;
}
//返回余数
return res%mod;
}
//高精度乘法+高精度求模
unsigned long long bigIntMult(unsigned long long a,unsigned long long b,unsigned long long mod)
{
unsigned long long aa,bb,r,j;
int i;
int stra[1000],strb[1000],c[2000];
aa=toString(stra,a,sizeof(stra)/sizeof(stra[0]));
bb=toString(strb,b,sizeof(strb)/sizeof(strb[0]));
r=toString(c,0,sizeof(c)/sizeof(c[0]));
//各位相乘
for(i=0;i<aa;i++)
{
for(j=0;j<bb;j++)
{
c[i+j]+=stra[i]*strb[j];
}
}
//进位(相乘跟进位这两步可以优化成一步)
for(i=0;i<sizeof(c)/sizeof(c[0]);i++)
{
if(c[i]>=10)
{
c[i+1]+=c[i]/10;
c[i]%=10;
}
}
//求模
//9.26 18:10
// i--;
while(i>0&&c[--i]==0);
unsigned long long res=0;
for(;i>=0;i--)
{
res=(res*10+c[i])%mod;
}
//返回余数
return res;
}
int main()
{
unsigned long long num;
srand((unsigned)time(NULL));
while(scanf("%llu",&num)!=EOF)
{
// srand((unsigned)time(NULL));
// printf("139\n");
fermat(num);
}
return 0;
}
提交上去后AC ,耶~~~~~~~~~~~刚开始学习算法,如果你们发现有什么不对或者发现什么错误的地方,请帮我指出来哈,我会改正的。