(轉載)O(N)的素數篩選法和歐拉函數
轉自:http://blog.csdn.net/dream_you_to_life/article/details/43883367 作者:Sky丶Memory
1.一個數是否為質數的判定.
質數,只有1和其本身才是其約數,所以我們判定一個數是否為質數,只需要判定2~(N - 1)中是否存在其約數即可,此種方法的時間復雜度為O(N),隨著N的增加,效率依然很慢。這裏有個O()的方法:對於一個合數,其必用一個約數(除1外)小於等於其平方根(可用反證法證明),所以我們只需要判斷2~之間的數即可.
1 bool is_prime(int num)
2 {
3 const int border = sqrt(num);
4 for (int i = 2; i <= border; ++i)
5 if (num % i == 0)
6 return false;
7 return 1 != num;
8 }
2.一個數的質因數分解
對於一個數N的質因數分解,簡單一點的方法通過枚舉2~N之間的每個數字,如果N值能整除當前枚舉的數,則將N值除盡,重復上面的步驟,直到結束.我們可以看出此種方法的時間復雜度為O(N),而我們通過上面介紹的方法,可以將時間復雜度降為O(),原理與判定一個數是否為質數是一樣的.
1 map<int, int> factor(int num)
2 {
3 map<int, int> ans;
4 const int border = sqrt(num);
5 for (int i = 2; i <= border; ++i)
6 while (num % i == 0)
7 ++ans[i], num /= i;
8 if (num > 1)
9 ans[num] = 1;
10 return ans;
11 }
3.歐拉函數
在數論中,對正整數n,歐拉函數是小於或者等於n的數中與n互質的數的個數.假設n的唯一分解式為,根據容斥原理可知
對於{p1,p2,....,pk}的任意子集S,“不與其中任何一個互述素”的元素個數為。不過這一項的前面是加號還是減號呢?取決於S中元素的個數-———奇數個數就是"減號”,偶數個數就是“加號”,如果對這個地方有疑問的,可以參考下組合數學容斥原理的章節.
現在我們得到了計算歐拉函數的公式,不過這樣計算起來非常麻煩。如果根據公式直接計算,最壞情況下需要計算項的多項式。不過這點倒不用我們擔心,前人已經在此公式上面已經做了相應的研究,這裏直接給出公式的變形,上述公式可以變形成如下的公式:
從而我們計算某個數的歐拉函數,只需要O(K)的計算時間,在剛才原始的基礎上大大提高了效率。如果題目中沒有給出唯一分解式,我們可以根據第二個小節的做法,在的時間復雜度解決這個問題.
1 int euler(int n)
2 {
3 const int border = sqrt(n);
4 int cnt = n;
5 for (int i = 2; i <= border; ++i)
6 {
7 if (n % i == 0)
8 {
9 cnt = cnt / i * (i - 1);
10 while (n % i == 0)
11 n /= i;
12 }
13 }
14 if (n > 1)
15 cnt = cnt / n * (n - 1);
16 return cnt;
17 }
上面介紹了一些關於素數和歐拉函數的小知識點,那現在進入主題——如何在O(N)的時間復雜度內求出某段範圍的素數表.在ACM比賽中,有些題目往往需要求出某段範圍內素數,而此時如何高效的求出素數表就顯得尤為重要。關於素數表的求法,比較出名的是埃氏素數篩選法。其基本原理是每找到一個素數,將其倍數的數從素數表中刪除,不斷重復此過程,最終表中所剩數據全部為素數。下面的gif圖片展示了該方法的相應步驟:
O(N)的素數篩選法和歐拉函數
埃氏素數篩選法的寫法有多種版本,其時間復雜度為,這裏給出一份實現代碼.
1 const int N = 1e+6 + 7;
2 bool prime[N];
3 void init_prime_table(int n)
4 {
5 const int border = sqrt(n);
6 memset(prime, true, sizeof(prime));
7 prime[0] = prime[1] = false;
8 for (int i = 2; i <= border; ++i)
9 {
10 if (!prime[i])
11 continue;
12 //此處j值需要註意溢出的bug
13 for (long long j = i * i; j <= n; j += i)
14 prime[j] = false;
15 }
16 }
一般情況下,對於大部分的題目上面的寫法已經夠用了.然而,有人將上述的方法優化到了,效率雖然沒有很大數量級的提升,不過,思想還是值得學習的.學過數學知識的人大都知道,對於一個正整數,如果其為合數,那麽該數的質因數分解形式是唯一的。假設一個合數n的質因數分解形式為:
現定義:對於某個範圍內的任意合數,只能由其最小的質因子將其從表中刪除。我們很容易得出該算法的時間復雜度為線性的,為什麽呢?因為一個合數的質因數分解式是唯一的,而且我們定義了合數只能由最小質因子將其從表中刪除,所以每個合數只進行了一次刪除操作(需要註意的是:埃氏素數篩選法中合數可能被多個質數刪除,比如12,18等合數).現在原始的問題轉換為怎麽將合數由其最小的質因子刪除?我們考查任何一個數n,假設其最小質因子為m,那麽小於等於m的質數與n相乘,會得到一個更大的合數,且其最小質因數為與n相乘的那個質數,而該合數可以直接從表中刪除,因為其剛好滿足之前的合數刪除的定義,所以我們需要維護一個表用來記錄已經找到了的質數,然後根據剛才敘述的步驟執行,就能將埃氏素數篩選法的時間復雜度降為.
1 const int N = 1e+6 + 7;
2 bool prime[N];
3 int rec[N], cnt;
4 void init_prime_table(int n)
5 {
6 cnt = 0;
7 memset(prime, true, sizeof(prime));
8 prime[0] = prime[1] = false;
9 for (int i = 2; i <= n; ++i)
10 {
11 if (prime[i])
12 rec[cnt++] = i;
13 //此處邊界判斷為rec[j] <= n / i,如果寫成i * rec[j] <= n,需要確保i * rec[j]不會溢出int
14 for (int j = 0; j < cnt && rec[j] <= n / i; ++j)
15 {
16 prime[i * rec[j]] = false;
17 if (i % rec[j] == 0)
18 break;
19 }
20 }
21 }
同樣的,通過此種方法,我們可以在線性的時間生成某段範圍的歐拉函數表,原理與上述類似,這裏就不做過多的解釋了。
(轉載)O(N)的素數篩選法和歐拉函數