[演算法] 線性篩
埃拉托斯特尼篩法
我們從大家國一就學過的埃拉托斯特尼篩法開始。
維護一個布林陣列not_prime,索引為那欄表示數字是否為質數,若為質數則該欄為0,若為合數則為1。
我之所以用0表示質數1表示合數是因為如此可省去初始化填入
1的步驟。
而埃拉托斯特尼篩法的邏輯就是先預設所有數都是質數,再一步步把不是的劃掉
因為大家都學過這個篩法的步驟和原理(去問你國中數學老師),以下直接上程式
std::vector<int> prime;
bool not_prime[N+1];
void Eratosthenes(int n) {
not_prime[0] = not_prime[1] = true;
for (int i=2; i<=n; i++) {
if (!not_prime[i]) {
prime.push_back(i); // 將質數加入 prime
if (1LL*i*i > n) continue;
for (int j=i*i; j<=n; j+=i) not_prime[j] = true;
// 之所以從i*i開始是因為比這個數小的i的倍數i*k (k<i) 都已經被篩過了
}
}
}
可以用 Mertens 第二定理證明其時間複雜度為
我不會證
線性篩(歐拉篩法)
因為我這裡卡了很久,所以希望能把它解釋清楚讓更多人能茅塞頓開。
線性篩是埃拉托斯特尼篩法的優化版本。
埃拉托斯特尼篩法中,一個合數會被多次篩到,以為例,當迴圈跑到i=2時,會跑一次not_prime[60] = true;,i=3時也會再次執行not_prime[60] = true;,i=5時也會,但除了第一次,之後都只是花時間在做一件已經完成的事情(也就是重複的跑not_prime[60] = true;)。
線性篩的想法很簡單:
要是我們能讓每個合數都只被他最小的質因數篩掉就好了
如此,篩法的時間複雜度就會降到
原理解說
每個合數都可以被分解成,其中表示的最小質因數。
由於是的最小質因數,為最小的非1的因數,從而一定大於
我們以對合數進行分類,被分到同一類的合數,都能藉由同一個,乘以一個「比小的質數」表示出來。
因此我們能用一個for迴圈,當時,將所有可用乘一個質數表示出來的合數找出來。
比小是這個演算法能成功跑下去的關鍵,能讓i隨著for迴圈慢慢變大的過程中,動態添加新質數,而不用擔心會漏篩。
什麼意思呢?我們手動試一下就知道了。
- 首先,將2加入
prime。此時prime = {2}。將所有可以被表示為的合數標記出來,其中為小於等於的質數。因此只有4被標記為合數:not_prime[2*2] = 1。 - 接著,將3加入
prime,此時prime = {2,3}。將所有可以被表示為的合數標記出來,其中。因此6、9被標記為合數。 - 接著,4已被標記為合數,因此不加入
prime,此時prime = {2,3}。將所有可以被表示為的合數標記出來,其中為小於等於的質數。因此8、12被標記為合數。(注意:此時我們先忽略後面要加入的break。實際線性篩中i=4只會對8標記,而不會對12標記) - 接著,5還未被標記為合數,故為質數,加入
prime,此時prime = {2,3,5}。將所有可以被表示為的合數標記出來,其中為小於等於的質數。因此10、15、25被標記為合數。
不斷重複,直到。
這裡我們解釋兩個細節:
- 迴圈跑到時,如果是合數,他必可拆成比小的質數和另一數的乘積,從而在之前的步驟就被標記出來了,所以若沒被標記為合數,他就一定是質數。稍微延伸一下可知此時所有小於的質數此時也都被找出來了。
- 由於比小,所以當跑到時,所有比小的質數都已經在
prime裡了,所以不會漏篩合數。
從上我們知道了線性篩的步驟,也能理解線性篩是能正確運作的。現在剩最後一個問題:
如何確保每個數「只被最小質因數篩到」?
我們回到先前的例子,從繼續跑下去:
- 接著,6已被標記為合數,因此不加入
prime,此時prime = {2,3,5}。將所有可以被表示為的合數標記出來,其中為小於等於的質數。因此12, 18, 30被標記為合數。
我們發現12被重複標記了,往回看可以看到當時,被3這個質數篩掉了,但我們希望12是被他的最小質因數2篩掉啊!這代表我們在i=4時要做一些事,讓12這個數被留到i=6,才由2這個質數(因為,2是12的最小質因數)篩掉。
我們可以注意到prime中的質數是由小到大排列的,所以我們可以在迴圈中多加一個判斷式:對於每個質數,都判斷是否整除,這樣找出來的第一個質數便為的最小質因數。一旦遇到了的最小質因數,後面的數就不用再乘了,因為這些乘出來的數要留給他們的最小質因數篩。
看上去很抽象,我們一樣以小數字舉例:
i=4時,首先乘以p=2得到8,篩掉,沒問題,接著判斷發現,所以直接break;(即不用考慮後面的,因為的最小質因數已經為,所以的最小質因數不會比還大,故不該被比2大的質數(如這裡的3)給篩掉)i=6時,首先乘以p=2得到12,篩掉,沒問題,接著判斷發現,所以直接break;(後面的跟的最小質因數都2,因此不該在這時由3、5篩掉)
Code
#include<bits/stdc++.h>
std::vector<int> prime;
bool not_prime[N+1];
void pre(int n) {
for (int i=2; i<=n; i++) {
if (!not_prime[i]) prime.push_back(i);
for (int p : prime) {
if (i*p > n) break;
not_prime[i*p] = true;
if (i%p == 0) break;
}
}
}
線性篩求歐拉函數
在線性篩的過程中結合動態規劃,我們能達到意想不到的效果。首先是「歐拉函數」的計算。
歐拉函數表示小於等於且與互質的正整數個數。比如
因為1~10中,1,3,7,9共四個數與10互質。
歐拉函數的性質
- 若為質數,
- 若為質數,
- 歐拉函數為積性函數:若,則
- 歐拉函數一般式:,為的所有質因數的集合
原理分析
我們的目標是結合動態規劃(用前一個算後面的),並運用到線性篩能找出最小質因數的特性。
設為的最小質因數,令。
分為以下兩種情形:
情形一:
此時的所有質因數皆亦為的,因此
情形二:
此時和互質,因此
我們知道線性篩能夠將每個數都標記一次(看他是質數 / 合數),因此我們多加一個步驟:在標記同時記錄那個數的歐拉函數值是多少。
和之前相同,我們用i乘以質數表中的數字p來生成出所有的合數。
最後一個問題是,我們要確定在計算時,已經算好了,解釋也很簡單
由於,為的最小質因數,所以「計算」發生在迴圈中i跑到時。
我們再來看是在何時被算出來的。
若為合數,則的最小質因數至少為2,故必定在迴圈執行到之前就算出來了。
若為質數,則我們只要讓i跑到時,先計算,再計算,就能保證在之前算出來。
程式實作
#include<bits/stdc++.h>
std::vector<int> prime;
bool not_prime[N+1];
int phi[N+1];
void pre(int n) {
phi[1] = 1;
for (int i=2; i<=n; i++) {
if (!not_prime[i]) {
prime.push_back(i);
phi[i] = i-1; // 新增
}
for (int p : prime) {
if (i*p > n) break;
not_prime[i*p] = true;
if (i%p == 0) {
phi[i*p] = p*phi[i]; // 新增
break;
}
phi[i*p] = (p-1)*phi[i]; // 新增
}
}
}
線性篩求正因數個數
說明
用類似的分析方法,這次我們維護三個陣列:not_prime、num、d,其中num紀錄num[k]表示的最小質因數在的標準分解式的次數,d[k]表示的正因數個數。
- 對於質數,
- 對於合數,其中為的最小質因數
- 若,則、
- 若,則、
程式
#include<bits/stdc++.h>
std::vector<int> prime;
bool not_prime[N+1];
int num[N+1];
int d[N+1];
void pre(int n) {
d[1] = 1;
for (int i=2; i<=n; i++) {
if (!not_prime[i]) {
prime.push_back(i);
d[i] = 2;
num[i] = 1;
}
for (int p : prime) {
if (i*p > n) break;
not_prime[i*p] = true;
if (i%p == 0) {
num[i*p] = num[i] + 1;
d[i*p] = d[i]*(num[i*p]+1)/(num[i]+1);
break;
}
num[i*p] = 1;
d[i*p] = 2*d[i];
}
}
}
線性篩求積性函數
從上面兩個例子,我們其實可以將其推廣到更多的積性函數(如正因數和、莫比烏斯函數等),分析步驟如下:
- 考慮怎麼算,其中為質數
- 考慮將合數拆解成,其中為的最小質因數,怎麼在已知下計算出
- 分別考慮和的情形
接著照樣造程式即可!




