1. 程式人生 > >O(N)的素數篩選法和尤拉函式

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;
        }
    }
}
         同樣的,通過此種方法,我們可以線上性的時間生成某段範圍的尤拉函式表,原理與上述類似,這裡就不做過多的解釋了。