埃拉托斯特尼篩法

我們從大家國一就學過的埃拉托斯特尼篩法開始。
維護一個布林陣列not_prime,索引為ii那欄表示數字ii是否為質數,若為質數則該欄為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 第二定理證明其時間複雜度為O(nloglogn)O(n\log\log n)

我不會證

線性篩(歐拉篩法)

因為我這裡卡了很久,所以希望能把它解釋清楚讓更多人能茅塞頓開。

線性篩埃拉托斯特尼篩法的優化版本。
埃拉托斯特尼篩法中,一個合數會被多次篩到,以6060為例,當迴圈跑到i=2時,會跑一次not_prime[60] = true;i=3時也會再次執行not_prime[60] = true;i=5時也會,但除了第一次,之後都只是花時間在做一件已經完成的事情(也就是重複的跑not_prime[60] = true;)。
線性篩的想法很簡單:

要是我們能讓每個合數都只被他最小的質因數篩掉就好了

如此,篩法的時間複雜度就會降到O(n)O(n)

原理解說

每個合數KK都可以被分解成pKiKp_K\cdot i_K,其中pKp_K表示KK的最小質因數。
由於pKp_KKK的最小質因數,為KK最小的非1的因數,從而iKi_K一定大於pKp_K

我們以iKi_K對合數KK進行分類,被分到同一類的合數,都能藉由同一個iKi_K,乘以一個「比iKi_K小的質數pKp_K」表示出來。
因此我們能用一個for迴圈,當i=iKi=i_K時,將所有可用iKi_K乘一個質數表示出來的合數K=piKK=p\cdot i_K找出來。
pKp_KiKi_K是這個演算法能成功跑下去的關鍵,能讓i隨著for迴圈慢慢變大的過程中,動態添加新質數,而不用擔心會漏篩。
什麼意思呢?我們手動試一下就知道了。

  • 首先i=2i=2,將2加入prime。此時prime = {2}。將所有可以被表示為p2p\cdot 2的合數標記出來,其中pp為小於等於i(=2)i(=2)的質數。因此只有4被標記為合數:not_prime[2*2] = 1
  • 接著i=3i=3,將3加入prime,此時prime = {2,3}。將所有可以被表示為p3p\cdot 3的合數標記出來,其中pi(=3)p\leq i(=3)。因此6、9被標記為合數。
  • 接著i=4i=4,4已被標記為合數,因此不加入prime,此時prime = {2,3}。將所有可以被表示為p4p\cdot 4的合數標記出來,其中pp為小於等於i(=4)i(=4)的質數。因此8、12被標記為合數。(注意:此時我們先忽略後面要加入的break。實際線性篩中 i=4 只會對8標記,而不會對12標記)
  • 接著i=5i=5,5還未被標記為合數,故為質數,加入prime,此時prime = {2,3,5}。將所有可以被表示為p5p\cdot 5的合數標記出來,其中pp為小於等於i(=5)i(=5)的質數。因此10、15、25被標記為合數。

不斷重複,直到i=Ni=N

這裡我們解釋兩個細節:

  1. 迴圈跑到i=iKi=i_K時,如果iKi_K是合數,他必可拆成比iKi_K小的質數和另一數的乘積,從而在之前的步驟就被標記出來了,所以若iKi_K沒被標記為合數,他就一定是質數。稍微延伸一下可知此時所有小於iKi_K的質數此時也都被找出來了。
  2. 由於pKp_KiKi_K小,所以當ii跑到iKi_K時,所有比iKi_K小的質數pKp_K都已經在prime裡了,所以不會漏篩合數。

從上我們知道了線性篩的步驟,也能理解線性篩是能正確運作的。現在剩最後一個問題:
如何確保每個數「只被最小質因數篩到」?
我們回到先前的例子,從i=6i=6繼續跑下去:

  • 接著i=6i=6,6已被標記為合數,因此不加入prime,此時prime = {2,3,5}。將所有可以被表示為p6p\cdot 6的合數標記出來,其中pp為小於等於i(=6)i(=6)的質數。因此12, 18, 30被標記為合數。

我們發現12被重複標記了,往回看可以看到當i=4i=4時,12(=34)12(=3\cdot 4)被3這個質數篩掉了,但我們希望12是被他的最小質因數2篩掉啊!這代表我們在i=4時要做一些事,讓12這個數被留到i=6,才由2這個質數(因為12=2612=2\cdot 6,2是12的最小質因數)篩掉。
我們可以注意到prime中的質數是由小到大排列的,所以我們可以在迴圈中多加一個判斷式:對於每個質數pp,都判斷pp是否整除ii,這樣找出來的第一個質數便為ii的最小質因數。一旦遇到了ii的最小質因數,後面的數就不用再乘了,因為這些乘出來的數要留給他們的最小質因數篩。
看上去很抽象,我們一樣以小數字舉例:

  • i=4時,首先乘以p=2得到8,篩掉,沒問題,接著判斷發現242\mid4,所以直接break;(即不用考慮後面的34=123\cdot 4=12,因為i(=4)i(=4)的最小質因數已經為22,所以pip\cdot i的最小質因數不會比22還大,故不該被比2大的質數(如這裡的3)給篩掉)
  • i=6時,首先乘以p=2得到12,篩掉,沒問題,接著判斷發現262\mid6,所以直接break;(後面的363\cdot 6565\cdot 6的最小質因數都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;
    }
  }
}

線性篩求歐拉函數

在線性篩的過程中結合動態規劃,我們能達到意想不到的效果。首先是「歐拉函數」的計算。
歐拉函數ϕ(n)\phi(n)表示小於等於nn且與nn互質的正整數個數。比如ϕ(10)=4\phi(10)=4
因為1~10中,1,3,7,9共四個數與10互質。

歐拉函數的性質

  1. pp為質數,ϕ(p)=p1\phi(p)=p-1
  2. pp為質數,ϕ(pk)=pkpk1=pk(11p)\phi(p^k)=p^k-p^{k-1}=p^k\left(1-\frac{1}{p}\right)
  3. 歐拉函數為積性函數:若(a,b)=1(a,b)=1,則ϕ(ab)=ϕ(a)ϕ(b)\phi(ab)=\phi(a)\phi(b)
  4. 歐拉函數一般式:ϕ(n)=npS(11p)\phi(n)=n\prod\limits_{p\in S}\left({1-\frac{1}{p}}\right)SSnn的所有質因數的集合

原理分析

我們的目標是結合動態規劃(用前一個算後面的),並運用到線性篩能找出最小質因數的特性。
pnp_nnn的最小質因數,令n=npnn^\prime=\frac{n}{p_n}
分為以下兩種情形:
情形一:pnnp_n \mid n^\prime
此時nn的所有質因數皆亦為nn^\prime的,因此

ϕ(n)=n(11pi)=pnn(11pi)=pnϕ(n)\phi(n)=n\prod\left({1-\frac{1}{p_i}}\right)=p_nn^\prime\prod\left({1-\frac{1}{p_i}}\right)=p_n\phi(n^\prime)

情形二:pnnp_n \nmid n^\prime
此時nn^\primenn互質,因此

ϕ(n)=ϕ(pn)ϕ(n)=(pn1)ϕ(n)\phi(n)=\phi(p_n)\phi(n^\prime)=(p_n-1)\phi(n^\prime)

我們知道線性篩能夠將每個數都標記一次(看他是質數 / 合數),因此我們多加一個步驟:在標記同時記錄那個數的歐拉函數值是多少。
和之前相同,我們用i乘以質數表中的數字p來生成出所有的合數。
最後一個問題是,我們要確定在計算ϕ(n)\phi(n)時,ϕ(n)\phi(n^\prime)已經算好了,解釋也很簡單
由於n=pnnn=p_nn^\primepnp_nnn的最小質因數,所以「計算ϕ(n)\phi(n)」發生在迴圈中i跑到nn^\prime時。
我們再來看ϕ(n)\phi(n^\prime)是在何時被算出來的。
nn^\prime為合數,則nn^\prime的最小質因數至少為2,故ϕ(n)\phi(n^\prime)必定在迴圈執行到i=n2ni=\lceil\frac{n^\prime}{2}\rceil\leq n^\prime之前就算出來了。
nn^\prime為質數,則我們只要讓i跑到nn^\prime時,先計算ϕ(n)=n1\phi(n^\prime)=n^\prime-1,再計算ϕ(n)\phi(n),就能保證ϕ(n)\phi(n^\prime)ϕ(n)\phi(n)之前算出來。

程式實作

#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_primenumd,其中num紀錄num[k]表示kk的最小質因數在kk的標準分解式的次數,d[k]表示kk的正因數個數。

  1. 對於質數ppnum(p)=1,d(p)=2num(p) = 1, d(p) = 2
  2. 對於合數n=pnnn=p_nn^\prime,其中pnp_nnn的最小質因數
    1. pnnp_n \mid n^\prime,則num(n)=num(n)+1num(n)=num(n^\prime)+1d(n)=d(n)num(n)+1num(n)+1d(n)=d(n^\prime)\frac{num(n)+1}{num(n^\prime)+1}
    2. pnnp_n \nmid n^\prime,則num(n)=1num(n)=1d(n)=d(n)(num(n)+1)=2d(n)d(n)=d(n^\prime)(num(n)+1)=2d(n^\prime)

程式

#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];
    }
  }
}

線性篩求積性函數

從上面兩個例子,我們其實可以將其推廣到更多的積性函數f(x)f(x)(如正因數和、莫比烏斯函數等),分析步驟如下:

  1. 考慮怎麼算f(p)f(p),其中pp為質數
  2. 考慮將合數拆解成n=pnnn=p_nn^\prime,其中pnp_nnn的最小質因數,怎麼在已知f(n)f(n^\prime)下計算出f(n)f(n)
    • 分別考慮pnnp_n \mid n^\primepnnp_n \nmid n^\prime的情形

接著照樣造程式即可!

參考資料