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