[英]Fast algorithm for finding the nearest smaller prime number
例如:- 如果給定數字是 10,我們必須返回 7(因為它是最接近的較小素數)
我能想到的方法是這樣的:-
Mainloop:測試給定數字是否為素數(通過應用素數測試),
如果它是素數,則返回數字,否則將數字減 1 並轉到 Mainloop。
但我必須在 long long int 范圍上工作,這需要很多時間。
有沒有更好的方法,如果我應該用上述方法 go 只有那么我應該使用哪個素性測試?
如果輸入的大小有界,則在預先計算的素數表中查找可能是最快的。
除上述內容外,還應注意Bertrand的假設指出,始終存在至少一個質數p,其中n<p<2n-2
。 這樣就為您提供了一個上限。
查看Miller-Rabin素數檢驗 。 這是概率性的,但是如果您執行數百次,則幾乎可以保證long long
內的精度。
另外,如果可以使用Java,則BigInteger.isProbablePrime
也可以提供幫助。 C \\ C ++似乎沒有用於測試素數的內置函數。
這是Daniel Fischer在評論中提到的Baillie-Wagstaff偽素性測試的偽代碼實現。 我們從一個簡單的Eratosthenes篩子開始,稍后我們將需要它。
function primes(n)
ps := []
sieve := makeArray(2..n, True)
for p from 2 to n step 1
if sieve(p)
ps.append(p)
for i from p * p to n step p
sieve[i] := False
return ps
powerMod
函數將所有基數b以m為模將底數b增大到指數e ; 它比先執行取冪然后取結果的模數要快得多,因為中間計算量很大。
function powerMod(b, e, m)
x := 1
while e > 0
if e % 2 == 1
x := (b * x) % m
b := (b * b) % m
e := floor(e / 2)
return x
數論的jacobi
函數表明a是否為二次余數mod p 。
function jacobi(a, p)
a := a % p
t := 1
while a != 0
while a % 2 == 0
a := a / 2
if p % 8 == 3 or p % 8 == 5
t := -t
a, p := p , a # swap
if a % 4 == 3 and p % 4 == 3
t := -t
a := a % p
if p == 1 return t else return 0
加里·米勒(Gary Miller)的強偽素數檢驗基於皮埃爾·德·費馬(Pierre de Fermat)的Little定理 ,該定理指出,如果p是素數,則對於任何a != 0, a ^( p -1)== 1(mod p )。 Miller的測試比Fermat的測試要強一些,因為它不能被Carmichael Numbers欺騙。
function isStrongPseudoprime(n, a)
d := n - 1; s := 0
while d % 2 == 0
d := d / 2; s := s + 1
t = powerMod(a, d, n)
if t == 1 return ProbablyPrime
while s > 0
if t == n - 1 return ProbablyPrime
t := (t * t) % n; s := s - 1
return Composite
Miller-Rabin測試執行k個強偽素數測試,其中k通常在10到25之間。強偽素數測試可以被欺騙,但是如果您執行了足夠多的測試,被欺騙的可能性很小。
function isPrime(n) # Miller-Rabin
for i from 1 to k
a := randInt(2 .. n-1)
if not isStrongPseudoprime(n, a)
return Composite
return ProbablyPrime
該素性測試足以滿足大多數目的,並且速度足夠快。 但是,如果您想要更強一點,更快一點的東西,則可以使用基於Lucas鏈的測試。 這是盧卡斯鏈的計算。
function chain(n, u, v, u2, v2, d, q, m)
k := q
while m > 0
u2 := (u2 * v2) % n; v2 := (v2 * v2 - 2 * q) % n
q := (q * q) % n
if m % 2 == 1
t1 := u2 * v; t2 := u * v2
t3 := v2 * v; t4 := u2 * u * d
u, v := t1 + t2, t3 + t4
if u % 2 == 1 u := u + n
if v % 2 == 1 v := v + n
u, v, k := (u / 2) % n, (v / 2) % n), (q * k) % n
m := floor(m / 2)
return u, v, k
由於約翰·塞爾弗里奇,通常使用算法初始化盧卡斯鏈。
function selfridge(n)
d, s := 5, 1; ds := d * s
repeat
if gcd(ds, n) > 1 return ds, 0, 0
if jacobi(ds, n) == 1 return ds, 1, (1 - ds) / 4
d, s := d + 2, s * -1; ds := d * s
然后,Lucas偽素數測試確定一個數字是素數還是可能是合成數。 像Fermat測試一樣,它具有標准和強壯兩種風格,並且像Fermat測試一樣,它可以被愚弄,盡管使用Fermat測試時,錯誤之處在於復合數字可能被錯誤地報告為質數,但是使用Lucas測試時,則錯誤是素數可能不正確地報告為合成。
function isLucasPseudoprime(n) # standard
d, p, q := selfridge(n)
if p == 0 return n == d
u, v, k := chain(n, 0, 2, 1, p, d, q, (n + 1) / 2)
return u == 0
function isLucasPseudoprime(n) # strong
d, p, q := selfridge(n)
if p == 0 return n == d
s, t := 0, n + 1
while t % 2 == 0
s, t := s + 1, t / 2
u, v, k := chain(n, 1, p, 1, p, d, q, t // 2
if u == 0 or v == 0 return Prime
r := 1
while r < s
v := (v * v - 2 * k) % n; k := (K * k) % n
if v == 0 return Prime
return ProbablyComposite
然后,Baillie-Wagstaff檢驗很簡單。 首先檢查輸入是否小於2或正整數(檢查平方根是否為整數)。 然后按小於100的素數進行試驗除法,很快就找到了大多數復合詞,最后對基數2進行了強大的偽素數測試(當然,有些人在基數3上添加了強大的偽素數測試),再由Lucas偽素數測試進行了最終確定。 。
function isPrime(n) # Baillie-Wagstaff
if n < 2 or isSquare(n) return False
for p in primes(100)
if n % p == 0 return n == p
return isStrongPseudoprime(n, 2) \
and isLucasPseudoprime(n) # standard or strong
Baillie-Wagstaff檢驗沒有已知的錯誤。
一旦有了良好的素數測試,就可以通過從n倒數到第一個素數停止,找到小於n的最大素數。
如果您對使用質數編程感興趣,我建議在我的博客中推薦這篇文章 ,或與質數有關的許多其他博客條目,您可以通過在博客上使用搜索功能找到這些文章。
你有非常有趣的任務!
我決定在純 C++(編譯時需要 C++20 標准)中從頭開始為您實現非常先進、大型但快速(高效)的解決方案。
在我的代碼中使用了以下補充算法 -埃拉托色尼篩法、費馬素性檢驗、 米勒拉賓素性檢驗、試驗除法(使用素數)、二分搜索。
還使用外部 C++ 庫Boost Multiprecision來實現大 integer 算術。 如果您願意,您可以使用其他庫,我的代碼是以可以使用任何庫的方式制作的。 CLang/GCC 編譯器也有__int128
類型,如果您的任務僅在 128 位范圍內,您也可以使用它來代替大整數。
我的代碼支持任何位大小的整數,但我徹底測試了正確性和速度直到 1024 位(即測試了 8、16、32、64、128、256、512、1024 位)。
我盡可能使用多核進行所有計算,如果您有 8 個內核(實際上是硬件線程),那么在所有計算中我啟動一個 8 個線程池並使用它們。 但是這種多線程方法僅用於大於 128 位的整數,這是一種默認啟用的啟發式算法,但如果需要,您可以通過將額外參數設置為主要 function PrevPrime()
。
我的主要算法( PrevPrime()
函數)執行以下步驟:
有一次(整個程序運行一次)我使用埃拉托色尼篩法計算了一張小於 2^20 的素數表。 該表最多可容納 1024 位整數。 如果您使用 2048 位或更大,則可以使用更多的素數。
當我們給定 N 時,我開始從 N 向下移動,並再次使用埃拉托色尼篩從范圍(N - 長度,N] 中過濾出合數。並非所有 2^20 素數都被使用,我通過實驗計算了哪些素數對於每個輸入 N 位大小是最佳的(對於 32 位 100 個素數就足夠了,對於 64 位 500 個素數,對於 128 位 - 1200 個素數,對於 256 位 - 7000 個素數,對於 512 位 - 17500 個素數,對於 1024 位 - 75000 個素數)。
埃拉托色尼篩需要從某個偏移量N - off
開始篩選每個特定的素數p
,其中off = N % p
。 因為除法在 CPU 的基本操作中是最昂貴的,所以我使用多線程(多核)方法(對於更大的整數,使用更小的單線程)來計算偏移量。
篩分完成后,我們確定范圍內的哪些數字(N - 長度,N] 是復合的,但我們不確定 rest 是素數還是復合數。因此,最后一步是對剩余的每個數字運行 Miller-Rabin 的素性檢驗數字。這些測試也是使用多線程方法完成的(僅適用於更大的整數)。
PrevPrime() 的計時在我的舊的慢速1.2 GHz
CPU 上進行,它有 8 個硬件線程,不同位大小的整數有不同的計時:
32位 - 每一個0.55
() 小於0.00005 sec
integer , 64位 - 0.0005 sec
, 128位 - 0.002
秒, 256位 - 0.012
秒, 512位 - 0.07
- 位 - 0.1
秒- 1.3
秒。
所以你可以看到,在我的慢速 1.2 GHz CPU 1024 位 integer 上只需 1 秒即可輸出前一個素數。
上述時序不僅取決於整數的位大小,還取決於素數的密度。 眾所周知,對於位數為B
的素數在ln(2^B)
中平均出現一次。 因此更大的數字不僅對大整數有更困難的數學運算,而且還有更多稀有的素數,因此更大的數字在輸出前一個素數之前需要檢查更多的數字。
為了查看 PrevPrime() 中的篩選效率如何,我提供了 Fermat 和 Miller-Rabin 測試本身的時間(下面的F
是 Fermat, MR
是 Miller-Rabin):
128位 - 復合 MR 和 F - 0.0002
秒,素數 MR - 0.0026
秒 F - 0.0054
秒; 256位 - 復合 MR 和 F - 0.0007
秒,素數 MR - 0.0116
秒 F - 0.0234
秒; 512位 - 復合 MR & F - 0.0043
秒,素數 MR - 0.0773
秒 F - 0.1558
秒; 1024位 - 復合 MR 和 F - 0.0290
秒,素數 MR - 0.4518
秒 F - 0.9085
秒
從上面的統計數據我們可以得出結論,在針對相同的故障概率時,費馬測試比米勒拉賓花費的時間多兩倍。
正如你所看到的,對於 1024 位,單個 Miller-Rabin 的素數大約是 0.5 秒,而整個 PrevPrime() 運行在 1 秒內,這意味着 PrevPrime() 非常高效,它只比使用素數的單次測試慢兩倍米勒-拉賓。
當然,您可能會注意到我使用了概率素數測試,因為 1024 位數字確定性測試需要非常多的時間。
對於概率測試,我選擇的目標失敗概率等於 1/2^32 = 1/4294967296,因此素數測試失敗的情況非常罕見。 您可以選擇較小的概率,如果您需要更高的精度,它可以在我的程序中進行調整。
從理論上講,Fermat 的每個單一測試給出 1/2 的失敗機會(不包括幾乎 100% 的失敗機會的卡邁克爾數),而 Miller-Rabin 給出 1/4 的失敗機會。 因此,要達到目標概率 1/2^32,需要 32 步費馬和 16 步米勒拉賓。
每個位大小所需的素數閾值是可自動調整的,但需要大量的單次預計算,這就是我計算表並對其進行硬編碼的原因,但計算表的代碼仍然存在於我的代碼中。 If you need to re-compute thresholds then go to function ComputePrevPrimeOptimalPrimesCnt()
and comment out first line (with return bits <= 32? 128: .....
) and run program, it will output sizes optimal for your PC and bit -尺寸。 但我預先計算的表格應該適用於任何 CPU,只需要調整新的位大小。
如果您不知道我唯一依賴的庫是Boost 。 它可以通過sudo apt install libboost-dev-all
輕松安裝在 Linux 下。 在 Windows 下有點困難,但仍然可以通過Chocolatey 的 Boost Package安裝(做choco install boost-msvc-14.3
,但先安裝Chocolatey )。
默認情況下,當程序運行時,它會運行所有測試,測試是代碼中所有以宏TEST(TestName) {.....
開頭的函數。 如果您需要額外的測試,只需編寫一個像TEST(NewTest) {...code here... }
這樣的主體,不需要額外的測試注冊。 如果您確定在使用我的庫時也可以刪除所有 TEST() 函數,則不需要它們來運行,除非它們對徹底測試我的庫很有用。
重要通知!!! . 我在 1 天內編寫的這段代碼,所以它沒有經過很好的測試,並且可能在某處包含一些錯誤。 因此,如果不仔細查看代碼和/或進行額外測試,不建議直接在生產中使用它。 它更像是教育代碼,可以為您編寫自己的生產就緒代碼提供一種指導。
代碼如下。
(注意, Try it online!
在線運行限制為 128 位(可能為 1024 位),由於 GodBolt 服務器將程序總運行時間限制為 3 秒,下載后將test_till_bits = 128
更改為1024
)。
源代碼在這里。 托管在Github Gist 源代碼上。 不幸的是,由於 StackOverflow 帖子限制為 30K 符號,我無法在此處內聯完整的源代碼,因為只有我的源代碼有 30K 字節,不包括上面的長英文描述(額外的 10K)。 因此,我在 Github GIST(上面的鏈接)和 GodBolt 服務器( Try it online!
上面的鏈接)上共享源代碼,兩個鏈接都有完整的源代碼。
代碼控制台 output:
'Test GenPrimes' 0.006 sec
'Test GenPrimes_CntPrimes' 0.047 sec
'Test MillerRabin' 0.006 sec
Fermat_vs_MillerRabin: bits 8 prp_cnt 58 mr_time 0.0000|0.0000 f_time 0.0000|0.0000, total time 0.009 sec
Fermat_vs_MillerRabin: bits 16 prp_cnt 57 mr_time 0.0000|0.0000 f_time 0.0000|0.0000, total time 0.001 sec
Fermat_vs_MillerRabin: bits 32 prp_cnt 46 mr_time 0.0000|0.0000 f_time 0.0000|0.0000, total time 0.004 sec
Fermat_vs_MillerRabin: bits 64 prp_cnt 22 mr_time 0.0000|0.0005 f_time 0.0000|0.0011, total time 0.041 sec
Fermat_vs_MillerRabin: bits 128 prp_cnt 13 mr_time 0.0002|0.0028 f_time 0.0002|0.0056, total time 0.134 sec
Fermat_vs_MillerRabin: bits 256 prp_cnt 6 mr_time 0.0008|0.0120 f_time 0.0008|0.0245, total time 0.321 sec
Fermat_vs_MillerRabin: bits 512 prp_cnt 1 mr_time 0.0042|0.0618 f_time 0.0042|0.1242, total time 0.857 sec
Fermat_vs_MillerRabin: bits 1024 prp_cnt 1 mr_time 0.0273|0.4101 f_time 0.0273|0.8232, total time 3.807 sec
'Test IsProbablyPrime_Fermat_vs_MillerRabin' 5.176 sec
'Test PrevPrime_ReCheckWith_PrimesDiv_and_Fermat' 5.425 sec
PrevPrime 8-bit threads:1 0.0000 sec, avg distance 3.3, total time 0.001 sec
PrevPrime 8-bit threads:8 0.0000 sec, avg distance 3.3, total time 0.000 sec
PrevPrime 16-bit threads:1 0.0000 sec, avg distance 11.2, total time 0.000 sec
PrevPrime 16-bit threads:8 0.0000 sec, avg distance 11.2, total time 0.000 sec
PrevPrime 32-bit threads:1 0.0001 sec, avg distance 10.5, total time 0.001 sec
PrevPrime 32-bit threads:8 0.0001 sec, avg distance 10.5, total time 0.001 sec
PrevPrime 64-bit threads:1 0.0014 sec, avg distance 39.7, total time 0.025 sec
PrevPrime 64-bit threads:8 0.0012 sec, avg distance 39.7, total time 0.023 sec
PrevPrime 128-bit threads:1 0.0110 sec, avg distance 85.2, total time 0.170 sec
PrevPrime 128-bit threads:8 0.0084 sec, avg distance 85.2, total time 0.142 sec
PrevPrime 256-bit threads:1 0.0452 sec, avg distance 207.4, total time 0.570 sec
PrevPrime 256-bit threads:8 0.0331 sec, avg distance 207.4, total time 0.473 sec
PrevPrime 512-bit threads:1 0.1748 sec, avg distance 154.0, total time 2.246 sec
PrevPrime 512-bit threads:8 0.1429 sec, avg distance 154.0, total time 2.027 sec
PrevPrime 1024-bit threads:1 2.1249 sec, avg distance 379.4, total time 15.401 sec
PrevPrime 1024-bit threads:8 1.6030 sec, avg distance 379.4, total time 12.778 sec
'Test PrevPrime' 33.862 sec
'Program run time' 44.524 sec
看來您正在解決這個問題 。
正如@Ziyao Wei所說,您可以簡單地使用Miller-Rabin素數測試來解決它。
這是我的解決方案
#include<cstdio>
#include<cstdlib>
#include<ctime>
short T;
unsigned long long n;
inline unsigned long long multi_mod(const unsigned long long &a,unsigned long long b,const unsigned long long &n)
{
unsigned long long exp(a%n),tmp(0);
while(b)
{
if(b&1)
{
tmp+=exp;
if(tmp>n)
tmp-=n;
}
exp<<=1;
if(exp>n)
exp-=n;
b>>=1;
}
return tmp;
}
inline unsigned long long exp_mod(unsigned long long a,unsigned long long b,const unsigned long long &c)
{
unsigned long long tmp(1);
while(b)
{
if(b&1)
tmp=multi_mod(tmp,a,c);
a=multi_mod(a,a,c);
b>>=1;
}
return tmp;
}
inline bool miller_rabbin(const unsigned long long &n,short T)
{
if(n==2)
return true;
if(n<2 || !(n&1))
return false;
unsigned long long a,u(n-1),x,y;
short t(0),i;
while(!(u&1))
{
++t;
u>>=1;
}
while(T--)
{
a=rand()%(n-1)+1;
x=exp_mod(a,u,n);
for(i=0;i<t;++i)
{
y=multi_mod(x,x,n);
if(y==1 && x!=1 && x!=n-1)
return false;
x=y;
}
if(y!=1)
return false;
}
return true;
}
int main()
{
srand(time(NULL));
scanf("%hd",&T);
while(T--)
{
for(scanf("%llu",&n);!miller_rabbin(n,20);--n);
printf("%llu\n",n);
}
return 0;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.