Prime Number Generation: Sieve of Eratosthenes
程度★ 難度★★
Sieve of Eratosthenes
這是一個製作質數表的方法。通常簡稱為「篩法」。
列出所有正整數。從2開始,刪掉2的倍數。找下一個未被刪掉的數字,找到3,刪掉3的倍數。找下一個未被刪掉的數字,找到5,刪掉5的倍數。如此不斷下去,就能刪掉所有合數,找到所有質數。
欲刪掉質數i的倍數之時,早已刪掉1倍到i-1倍了,直接從i倍開始。
欲刪掉質數i的倍數之時,早已刪掉「小於i的質數、其倍數」倍了,直接刪掉「大於等於i的質數、其倍數」倍。
乍看下程式碼增多而變慢,實際上cache miss減少而變快。
一個合數x,必定有一個小於等於sqrt(x)的質因數。所有小於等於sqrt(x)的質數,刪掉這些質數的倍數,就能刪掉所有合數了。
顛倒true和false,節省初始化時間。
製作質數表。篩法結束之後,掃描一次陣列即可。
UVa 406 516 524 543 10140 10311
使用bitset來取代bool陣列
一個int有32個位元,可以當作32個欄位來使用,節省記憶體空間,減少cache miss。
不處理2的倍數
不處理2的倍數,節省一半記憶體,增進一點速度。
令陣列第0格代表數字1、陣列第1格代表數字3、陣列第2格代表數字5、……,以此類推。
時間複雜度
考慮迴圈索引值i與j共有多少種:i一共有N種;j一共有(N/p1 - p1) + (N/p2 - p2) + ... + (N/sqrtN - sqrtN)種,其中p1 p2 ...是質數2 3 ...。時間複雜度就是兩者相加,為O(NloglogsqrtN) = O(NloglogN)。
1/1 + 1/2 + ... + 1/N = O(logN) 1/p1 + 1/p2 + ... + 1/N = O(loglogN) 1/p1 + 1/p2 + ... + 1/sqrtN = O(loglogsqrtN) = O(loglogN)
http://en.wikipedia.org/wiki/Divergence_of_the_sum_of_the_reciprocals_of_the_primes 1/1 + 1/2 + ... + 1/n - ln(n) 趨近於Euler-Mascheroni constant 1/p1 + 1/p2 + ... + 1/n - ln(ln(n)) 趨近於Meissel–Mertens constant
至於迴圈最內部,每個合數都被刪掉一次,總共O(N)次。
線性時間篩法
一邊製作質數表,一邊刪掉每個數的質數倍,時間複雜度就能達到O(N)。然而實際執行速度並沒有更快。
Prime Number Generation: 6n±1 Method
程度★ 難度★
6n±1 Method
這是一個精簡版的篩法,原理是:只拿2和3這兩個質數先篩過一遍,剩下的數字則用試除法驗證是不是質數。
2和3的最小公倍數是6,我們就把所有數字分為6n、6n+1、6n+2、6n+2、6n+3、6n+4、6n+5六種(n是倍率)。除以六會餘零的數字為6n,除以六會餘一的數字為6n+1,以此類推。
可以看出6n、6n+2、6n+3、6n+4會是2或3的倍數,不屬於質數。因此,只要驗證6n+1和6n+5是不是質數就可以了。(6n+5也可以寫成6n-1,意義不變。)
6n-1到6n+1,再到下一個6n-1,再到6n+1,把這些要驗證的數字由小排到大,可以發現之間的差值會是2 4 2 4 2 4 ...不斷跳二跳四。實作程式碼時,就可以直接用加法加二加四, 而不必用乘法及加減法計算6n-1、6n+1,如此一來程式的執行效率會好一點。
驗證的順序是:數字2和3明顯的是質數,不必驗證;若是從數字5開始驗證,那麼下一個要驗證的數字就是5+2,再下一個就是5+2+4,再下一個就是5+2+4+2,如此不斷下去。
這個方法的時間複雜度是O(NsqrtN),空間複雜度是O(1)。事實上6n±1法比篩法慢上許多。不過6n±1法的程式碼構造較為單純,當要枚舉的質數範圍不大時,有機會跑得比篩法還快。另外,6n±1法不需要開一條超大陣列來做計算,比起篩法它節省了很多空間,這也是它的優點。
Primality Test
程度★ 難度★★★
Primality Test
質數測試,測試一個數字是否為質數。質數測試的方法相當多,其中有保證結果一定正確的方法,亦有結果不一定正確的方法。以下所介紹的,除了Divisibility Test的結果一定是正確的,其他方法的結果都不一定正確。
質數測試屬於P問題,不過以下介紹的皆非多項式時間的演算法,甚至是不保證結果正確的演算法。若對多項式時間、保證結果正確的演算法有興趣,可上網搜尋AKS Algorithm。
要進行質數測試,也可以直接用篩法把所有質數生出來,再來判斷質數。
Divisibility Primality Test
整除性測試法。依照質數定義,一個質數p不會被大於1且小於p的數字整除,只要把這些數字都拿來試除,就可以判定一個數字是不是質數。
撇開定義,這演算法其實就是窮舉所有可能的因數一一試除。
Factorization
程度★ 難度★★★
Factorization
因式分解。這裡談的是把一個正整數分解成質因數的連乘積:
n = 2^m1 * 3^m2 * 5^m3 * 7^m4 * 11^m5 * …
因數分解問題屬於NP問題,目前尚未有好解法。
Fundamental Theorem of Arithmetic(算數基本定理)
凡是正整數都可以藉由質因數分解成為一個獨一無二的式子,不同的n就會對應不同的(m1, m2, m3, …),反方向亦同。
根據算數基本定理,凡是牽扯到乘法、因數、倍數的數學運算,都可以改變成比較簡單的運算。
分解前 | 分解後
-----------+--------------------------------------
乘除法 | 加減法
x * y | (a1+b1, a2+b2, ...)
|
整除 | 大於等於
x % y = 0 | (a1, a2, ...) ≥ (b1, b2, ...)
y | x | a1≥b1 and a2≥b2 and ...
|
最大公因數 | 最小值
gcd(x, y) | (min(a1, b1), min(a2, b2), ...)
|
最小公倍數 | 最大值
lcm(x, y) | (max(a1, b1), max(a2, b2), ...)
|
互質 | 對應項至少有一項為零
x⊥y | min(a1, b1) = 0 and min(a2, b2) = 0 and ...
| a1*b1 = 0 and a2*b2 = 0 and ...
算術基本定理闡述了另一種世界觀,把數字看作是質數的結合。質數的英文prime有著「原始就有」的意思,便是指質數是所有數字的根本。
Trial Division Factorization Method
把所有可能的因數拿來試除。用質因數會更好。
當要因式分解的數字有很多筆時,可以先建好質數表,然後只拿質因數來試除。
UVa 516 583 10179 10290 10329 10392 10622 10780 10791
Fermat's Factorization Method
把一個數字分解成兩個數的乘積(這兩個數不一定會是質數)。一直遞迴分解下去,無法再分解的時候就找到質因數了。分解手法如下:
現在要找出 a 和 b 讓 n = a*b 使用公式 x^2 – y^2 = (x+y) * (x-y) 令 n = x^2 – y^2, a = x+y, b = x-y 窮舉整數 x,看看 sqrt(x^2-n) 是不是剛好就是一個整數 y, 如果是整數就找到一組 a 和 b 了。
Pollard's p-1 Factorization Method
這個方法可以找出n的其中一個質因數p。數學家Pollard證明當p-1的質因數都不大於b時,則p = gcd(2^b! - 1, n)。各位可以從wiki找到證明。:)
選定一個適當的b後,就可以進行演算法。有可能會找不到質因數。
Pollard's ρ Factorization Method
這個方法可以找出n的其中一個質因數p。有可能會失敗。
若有兩數x和y,p可以整除x-y,n不能整除x-y,則p = gcd(x-y, n)。
這個p可能是1、n、n的質因數,但是只要努力不懈地枚舉x和y,就有機會求得n的質因數。
以亂數產生器枚舉x和y
首先使用一個簡單的亂數產生器f(ai+1) = ai2 + c,以及亂數種子a0,其中c為整數常數。然後以(a1 , a0) (a2 , a1) (a3 , a2) … (an-1 , an-2)這些數對作為x和y。
讀者可在此思考一下:
一、為何亂數產生器不選用f(ai+1) = ai + c這條更加簡單的式子?
二、為何a0至少要是+2?
三、為何c不可以是0,也不可以是-2?讀者可將亂數產生器的式子,代入到x和y之中,計算一下x-y,並計算gcd(abs(x-y), n)。
四、如果我們將x和y對調,對結果有影響嗎?
五、為何p等於n的時候,就要結束迴圈,宣告失敗,不繼續找了?
小改進:x和y可以先模n,防止溢位
計算gcd(abs(x-y), n)的時候,其實就會拿abs(x-y)去模n。所以我們可以讓x和y每次都先模n,或者是讓亂數產生器每次都模n,這麼做不影響結果。不斷模n,可以防止x和y溢位,好處多多。
小改進:偵測循環
方才的枚舉方式並不完美。我們知道,在模n的情況下,x和y最終必會重複出現,產生循環。我們得在循環開始時,就立刻結束演算法,否則接下來就會一直遇到重複的數字,進行重覆的運算。況且越早進入循環,就有越多數字白算。
Pollard採用了Floyd's Cycle Finding Algorithm的概念來偵測循環,以(a1 , a2) (a2 , a4) (a3 , a6) … (an , a2n)這些數對作為x和y,一旦an等於a2n即發現循環(當有模n的情況下),馬上結束演算法。
演算法到此告一段落!各位在實作時,如果演算法失敗,可以更換a0和c,或許就可以找出質因數了!
至於這個演算法的名稱由來,是因為不斷枚舉a0 , a1 , a2 , ...,最終必會循環,繪圖時可以畫成一個ρ (rho)的形狀,因而得名。
非常可惜,沒有練習題!
Euler's Totient Function
程度★★ 難度★★
Euler's Totient Function(Euler's φ Function)
這是一個公式。計算1到n的正整數當中,跟n互質(最大公因數是一)的數,總共有幾個。
首先要將n做質因數分解:
n = p1a1 × p2a2 × … × pkak where p1 … pk are primes
以質因數計算Euler's Totient Function,φ唸做phi:
φ(n) = n × (1 - 1/p1) × (1 - 1/p2) × … × (1 - 1/pk)
可以採用這個順序計算,避免溢位:
n ÷ p1 × (p1-1) ÷ p2 × (p2-1) ÷ … ÷ pk × (pk-1)
如此就不用一個一個去計算最大公因數了,非常有效率!
當質因數分解採用試除法,計算Euler's Totient Function的時間複雜度為O(sqrtN)。預先建立質數表,得加速至O(π(sqrtN)),其中π(N)是1到N的質數個數。
UVa 10299 10179 11064
下面是Euler's Totient Function的一些性質,證明省略之:
φ(p) = p - 1 iff p is prime.
φ(pa) = pa - pa-1 iff p is prime.
φ(n × m) = φ(n) × φ(m) iff n and m are relatively prime.
φ(n) = φ(p1a1) × … × φ(pkak) iff n = p1a1 × … × pkak
where p1 … pk are prime.
建立表格
未經改良的篩法,能求出每個數的質因數。運用篩法計算Euler's Totient Function,時間複雜度為O(NloglogN)。
或者,首先運用篩法,為每個數求得一個質因數;然後運用Euler's Totient Function的性質,實施Dynamic Programming。此時得以套用線性時間篩法,時間複雜度為O(N)。
UVa 10820 10990 11327 11424 11426
延伸閱讀:最大公因數的傅立葉轉換就是Euler's Totient Function
http://en.wikipedia.org/wiki/Euler%27s_totient_function#Fourier_transform
N-1
φ(n) = Σ { gcd(n, k) ÷ ei*2π*(n/N)*k }
k=0
Möbius Function
程度★★ 難度★★★
Möbius Function
用排容原理求一個數字的所有因數總和。
ICPC 2116