前言
本文寫於email同學被巨水的素數篩教做人之后。
會提到兩種篩法:埃拉托色尼篩法,線性篩法。
知識儲備
1.對於一個合數x,必有一個范圍在2~√x 的因數。(顯然)
2.任何一個大於1的自然數都能被唯一分解有限個質數的乘積,如 X=P1 a1 *P2a2 *······* Pn an 其中P為質數,a為指數.
素數的判定(試除法)
字面意思,根據第一條性質,我們枚舉2~~√n所有數,用n去試着除以,若有能整除的n為合數,若都不能整除,n就是質數了。
int prime(int n) { for(int i=2;i*i<=n;i++) if(!(n%i)) return 0; return 1; }
時間復雜度O:(√n)
埃拉托色尼篩法(Eratosthenes)
這種篩法應該是是效率相對比較高,而且很好理解,用得最多的篩法。
試除法是用原數去嘗試諸多因子,那對於求一個范圍(比如1~N)的所有素數,顯然上述方法是不可取的。
那我們反向思考,嘗試枚舉因子去排除素數(即標記合數)。
枚舉2~N,並且用2~N的倍數去標記合數,比如枚舉到i,那2*i,3*i,4*i······,全部都標記為合數,直到k*i>N。
掃到數x時,看它是否有合數標記,如果沒有,則說明2~x-1的數中,都沒有它的因數,根據性質1,顯然x-1>√x(x>2),所以x為質數。
顯然,一個合數可能被多個小因數標記,這就是優化的下手處。
我們如果不從2*i開始篩,而從i*i開始篩是否可行呢?
顯然可行。因為我們掃到2的之后就已經用2篩完了所有2的倍數,掃到3的之后已經篩完了所有2,3的倍數,掃到4的之后已經篩完了所有2,3 ,4的倍數······
所以掃到i時,2~i-1的倍數已經全部被篩了,沒必要再篩i的2~i-1倍的數,可以直接從i2開始篩了。
void primes(int n) { memset(v,0,sizeof(v));//假設全是素數,無合數標記 for(int i=2;i<=n;i++) { if(!v[i]) { prime[++cnt]=i; for(int j=i*i;j<=n;j+=i) v[j]=1; } }
}
時間復雜度:O(∑ N/p ) =O(N loglogN ),p為小於N的質數 。接近線性效率,而且比較起線性篩是更靈活的(最后的例題會提到)
線性篩法
優化后的埃拉托色尼篩也有重復篩到數的情況,比如2,3都會篩12,2和4都會篩到24,要保證線性效率,就必須讓每個數只被篩一次
我們不能讓12=2*6,12=3*4發生,只能讓12=2*2*3發生。
所以我們需要將每個合數用它最小的質因子篩去。
標記合數時,我們只向現有的數中乘上一個質因子,這相當於合數的質因子從小到大累積。(意思是現在只能用現在掃到的這個數乘篩出來的素數來篩后面的素數)
因為找出篩的素數一定是最小素數,所以對於掃到的合數,它只能篩到出現它的約數之前的數的它的倍數。質數則能篩掉2*x~x*x。(小於x2的所有x的質數倍)
前面說得太抽象了,不理解不要緊,模擬一下。
prime[]數組裝素數,v[i]是篩掉i的那個最小質因數,若無標記說明是質數
1、掃到 2,v[2]無標記,賦值 :v[2]=2,prime[1]=2.
遍歷prime,用2*prime[j],可以篩掉4. 賦值: v[4]=2.
2、掃到3,v[3]無標記,賦值: v[3]=3,prime[2]=3.
遍歷prime,用3*prime[j],可以篩掉6,9 .賦值: v[6]=2,v[9]=3
3、掃到 4,v[4]有標記
遍歷prime,用4*prime[j],只能篩掉8,賦值:v[8]=2(不能篩12!!即便有3這個素數。因為3大於4的最小質因數2了,用3*4篩就違背了2*3*3的原理,這里能被3*4篩掉的12,后來也可以被2*6篩到,保證了篩到12的是最小質因數2)
4、掃到5,v[5]無標記,賦值: v[5]=5,prime[3]=5.
遍歷prime,用5*prime[j],可以篩掉10,15,25 .賦值: v[10]=2,v[15]=3,v[25]=5,
大概就是這樣了
void primes(int n) { memset(v,0,sizeof(v));//假設全是素數,無合數標記 for(int i=2;i<=n;i++) { if(!v[i]) { prime[++cnt]=i; v[i]=i; } for(int j=1;j<=cnt;j++) { if(prime[j]>v[i]||prime[j]*i>n)break; //出現了比i的最小質因數還小的質數(對於4來說出現了3) v[prime[j]*i]=prime[j]; } } for(int i=1;i<=cnt;i++) cout<<prime[i]<<" "; }
例題
題意
求出范圍[k,n]內的合數個數,並且求出篩掉每個合數的最小素數之和,答案對998244353取模
數據規模

分析
數據規模1e12太大了,線性篩也無法處理,但是區間大小只有1e7,所以我們可以用線性篩+埃拉托色尼篩
用線性篩預處理出1e6范圍內的所有素數,再用這些素數去做埃拉托色尼篩。
為什么不能都做線性篩?因為線性篩必須完全記錄了篩每一個數的那個素數,不能有任何無操作的空白,而埃拉托色尼篩就沒有這類限制,比較靈活,直接把篩出來的素數用來繼續篩。
注意數組開不下,需要移一下位。
#include<bits/stdc++.h> using namespace std; #define N 10000100 #define ll long long const ll limit=2000000; const ll mod=998244353; ll prime[N],v[N]; ll n,k,cnt,ans1,ans2; int main() { cin>>k>>n; for(ll i=2;i<=limit;i++) { if(!v[i]) { v[i]=i; prime[++cnt]=i; } for(ll j=1;j<=cnt;j++) { if(prime[j]>v[i]||prime[j]*i>limit)break; v[prime[j]*i]=prime[j]; } } //線性篩預處理 memset(v,0,sizeof(v)); for(ll i=1;i<=cnt;i++) { ll st=max((ll)1,(k-1)/prime[i])*prime[i]+prime[i];//計算出起始位置 for(ll j=st;j<=n;j+=prime[i]) { if(!v[j-k]) { v[j-k]=1; ans1++; ans2=(ans2+prime[i])%mod; } } } cout<<ans1<<" "<<ans2; return 0; }
總結
關於素數篩的整理到此告一段落,比較基礎的知識,兩種篩法各有好處,線性篩更快,但是埃拉托色尼篩可以節約空間,也比較適合做區間的跨度不大,但區間本身左右端點數值很大的題目。
