前言
本文写于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<" "; }
例题
题意
求出范围[k,n]内的合数个数,并且求出筛掉每个合数的最小素数之和,答案对998244353取模
数据规模
分析
数据规模1e12太大了,线性筛也无法处理,但是区间大小只有1e7,所以我们可以用线性筛+埃拉托色尼筛
用线性筛预处理出1e6范围内的所有素数,再用这些素数去做埃拉托色尼筛。
为什么不能都做线性筛?因为线性筛必须完全记录了筛每一个数的那个素数,不能有任何无操作的空白,而埃拉托色尼筛就没有这类限制,比较灵活,直接把筛出来的素数用来继续筛。
注意数组开不下,需要移一下位。
#includeusing 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< " "<<ans2; return 0; }
总结
关于素数筛的整理到此告一段落,比较基础的知识,两种筛法各有好处,线性筛更快,但是埃拉托色尼筛可以节约空间,也比较适合做区间的跨度不大,但区间本身左右端点数值很大的题目。