利用費馬小定理進行素性測試
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 ,耶~~~~~~~~~~~剛開始學習算法,如果你們發現有什麼不對或者發現什麼錯誤的地方,請幫我指出來哈,我會改正的。