線性篩的理解及應用


素數篩法

如果我們想要知道小於等於 $n$ 有多少個素數呢?

一個自然的想法是我們對於小於等於 $n$  的每個數進行一次判定。這種暴力的做法顯然不能達到最優復雜度,考慮如何優化。

考慮這樣一件事情:如果  是合數,那么  的倍數也一定是合數。利用這個結論,我們可以避免很多次不必要的檢測。

如果我們從小到大考慮每個數,然后同時把當前這個數的所有(比自己大的)倍數記為合數,那么運行結束的時候沒有被標記的數就是素數了。

int Eratosthenes(int n) {
  int p = 0;
  for (int i = 0; i <= n; ++i) is_prime[i] = 1;
  is_prime[0] = is_prime[1] = 0;
  for (int i = 2; i <= n; ++i) {
    if (is_prime[i]) {
      prime[p++] = i;  // prime[p]是i,后置自增運算代表當前素數數量
      for (int j = i * i; j <= n;
           j += i)  // 因為從 2 到 i - 1 的倍數我們之前篩過了,這里直接從 i
                    // 的倍數開始,提高了運行速度
        is_prime[j] = 0;  //是i的倍數的均不是素數
    }
  }
  return p;
}

以上為 Eratosthenes 篩法 (埃拉托斯特尼篩法),時間復雜度是 $O(nloglogn)$.

以上做法仍有優化空間,我們發現這里面似乎會對某些數標記了很多次其為合數。有沒有什么辦法省掉無意義的步驟呢?

答案當然是:有!

如果能讓每個合數都只被標記一次,那么時間復雜度就可以降到 $O(n)$了。

//這里再求素數的同時也計算了 $phi$ ,是為了看出素數篩與線性篩的聯系。

void init() {
  phi[1] = 1;
  for (int i = 2; i < MAXN; ++i) {
    if (!vis[i]) {
      phi[i] = i - 1;
      pri[cnt++] = i;
    }
    for (int j = 0; j < cnt; ++j) {
      if (1ll * i * pri[j] >= MAXN) break;
      vis[i * pri[j]] = 1;
      if (i % pri[j]) {
        phi[i * pri[j]] = phi[i] * (pri[j] - 1);
      } else {
        // i % pri[j] == 0
        // 換言之,i 之前被 pri[j] 篩過了
        // 由於 pri 里面質數是從小到大的,所以 i 乘上其他的質數的結果一定會被
        // pri[j] 的倍數篩掉,就不需要在這里先篩一次,所以這里直接 break
        // 掉就好了
        phi[i * pri[j]] = phi[i] * pri[j];
        break;
      }
    }
  }
}

關鍵之處在:if(i%prime[j]==0)  break;

這句代碼保證了每個數最多被篩一次,將時間復雜度降到了線性。

證:prime[]數組中的素數是遞增的,當i能整除prime[j],那么i*prime[j+1]這個合數肯定會被prime[j]乘以某個數篩掉。因此,這里直接break掉,將i*prime[j+1]及之后的給后面的數去篩。這種方法能保證每個數只被篩一遍,又能保證每個數都被篩到。

為了更好的理解,畫出前面幾次篩的情況:

 從圖上我們看到,第一列篩掉的是最小素因子是2的數,第二列篩掉的是最小素因子為3的數,依次類推,可以把所有的合數都篩掉。

因為是按照最小素因子篩選,每個數的最小素因數只有一個,所以可以保證每個數都只會被篩一遍。

例如,$i=6$ 時,第一個素數是2,能整除,篩掉12后就break;至於第二個素數3,6x3中的最小素因數肯定是前一個素數2,所以它要到 $i=9$,素數取2時才被篩掉。

上面的這種 線性篩法 也稱為 Euler 篩法 (歐拉篩法)。

歐拉篩的速度大概是埃氏的3~4倍,然而在小數據中卻有被完爆的可能(因為埃氏篩cache友好?)。

篩法求歐拉函數

注意到在線性篩中,每個合數都是被最小的質因數篩掉。比如設 $p_1$ 是 $n$ 的最小質因數,${n}' = \frac{n}{p_1}$,那么線性篩的過程中 $n$ 通過 ${n}' \times p_1$ 篩掉。

觀察線性篩的過程,我們還需要處理下面兩個部分,下面對 ${n}' \ mod \ p_1$ 分情況討論:

$$
\begin{aligned}
\varphi(n) &= n \times \prod_{i=1}^s\frac{p_i-1}{p_i}\\
&= p_1 \times {n}' \times  \prod_{i=1}^s\frac{p_i-1}{p_i}\\
&= p_1 \times \varphi({n}')
\end{aligned}$$

那如果 ${n}' \ mod \ p_1 \neq 0$ 呢,這時 ${n}'$ 和 $n$ 互質,根據歐拉函數的性質,我們有:

$$
\begin{aligned}
\varphi(n) & = \varphi(p_1) \times \varphi({n}')\\
&=(p_1-1) \times \varphi({n}')\\
\end{aligned}$$

//線性篩的寫法見前面,這里用的另外一種篩法

void phi_table(int n, int* phi) {
  for (int i = 2; i <= n; i++) phi[i] = 0;
  phi[1] = 1;
  for (int i = 2; i <= n; i++)
    if (!phi[i])
      for (int j = i; j <= n; j += i) {
        if (!phi[j]) phi[j] = j;
        phi[j] = phi[j] / i * (i - 1);
      }
}

線性篩

線性篩基本可以求出所有積性函數,盡管方法不盡相同。

注意一點,外層循環都是從2開始的。

篩法求莫比烏斯函數

由於 $\mu$ 函數是積性函數,因此可以線性篩。

void getMu(int n)
{
    mu[1] = 1;
    for(int i = 2;i <= n;i++)
    {
        if(!flg[i])  p[++tot] = i, mu[i] = -1;
        for(int j = 1;j <= tot && i*p[j] <= n;j++)
        {
            flg[i*p[j]] = 1;
            if(i % p[j] == 0)
            {
                mu[i*p[j]] = 0;
                break;
            }
            else
            {
                mu[i*p[j]] = -mu[i];
            }
        }
    }
}

篩法求約數個數

設 $n = {p_1}^{e_1}{p_2}^{e_2}...{p_r}^{e_r}$,易知約數個數為 $fac(n) = (1+e_1)(1+e_2)...(1+e_r)$。也很容易證明其為積性函數。

需要開一個輔助數組 $minp(i)$ 表示 $i$ 最小素因數的次冪.

①當 $i$ 為質數時,$fac(i)=2, \ midp(i)=1$.

②當 $i$ 與 $p$ 互質,$fac(i*p) = fac(i)*fac(p) = fac(i)*2, \ minp(i*p) = 1$

③當 $i$ 與 $p$ 不互質,但 $p$ 是 $i$ 的最小素因子,$fac(i*p) = \frac{fac(i)}{minp(i)+1} * (minp(i)+2), \ minp(i*p) = min(i) + 1$.

void getFac(int n)
{
    fac[1] = 1, minp[1] = 0;
    for(int i = 2;i <= n;i++)
    {
        if(!flg[i])  p[++tot] = i, fac[i] = 2, minp[i] = 1;
        for(int j = 1; j <= tot && i*p[j] <= n;j++)
        {
            flg[i*p[j]] = 1;
            if(i % p[j] == 0)
            {
                fac[i*p[j]] = fac[i] / (minp[i]+1) * (minp[i]+2);
                minp[i*p[j]] = minp[i] + 1;
                break;
            }
            else
            {
                fac[i*p[j]] = fac[i] * 2;
                minp[i*p[j]] = 1;
            }
        }
    }
}

篩法求約數和

設 $n = {p_1}^{e_1}{p_2}^{e_2}...{p_r}^{e_r}$,易知約數和為 $sumd(n) = \sum_{k=0}^{e_1}{p_1}^k \cdot  \sum_{k=0}^{e_2}{p_2}^k ... \sum_{k=0}^{e_r}{p_r}^k $.

需要設 $minp(i)$ 表示最小素因數的冪次方,

①當 $i$ 質數,$sumd(i) = i+1, \ midp(i)=i$

②當 $i$ 與 $p$ 互質時,$sum(i*p) = sum(i)*sum(p), \ midp(i) = minp(p) = p$

③當 $i$ 與 $p$ 不互質,但 $p$ 是 $i$ 的最小素因子,根據等比公式求出最小素因子的貢獻,除掉之前的乘上現在的,$sum(i*p) = \frac{sum(i)}{minp(i)-1} * (minp(i)^2-1), \ minp(i*p) = minp(i)*p$

void getSumd(int n)
{
    sumd[1] = 1, minp[1] = 0;
    for(int i = 2;i <= n;i++)
    {
        if(!flg[i])  p[++tot] = i, sumd[i] = 1+i, minp[i] = i;
        for(int j = 1;j <= tot && i * p[j] <= n;j++)
        {
            flg[i*p[j]] = 1;
            if(i % p[j] == 0)
            {
                sumd[i*p[j]] = sumd[i]  * (minp[i]*p[j]*p[j]-1) / (minp[i]*p[j]-1);  //注意溢出 
                minp[i*p[j]] = minp[i] * p[j];
                break;
            }
            else
            {
                sumd[i*p[j]] = sumd[i] * sumd[p[j]];
                minp[i*p[j]] = p[j];
            }
        }
    }
}

 

 

 

 

1.https://www.jianshu.com/p/f16d318efe9b

2. http://www.voidcn.com/article/p-uetmmxng-bha.html

3. https://oi-wiki.org/math/sieve/#_1

4.  https://blog.masterliu.net/algorithm/sieve/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM