学习笔记:数学-GCD与LCM-素数筛法

筛法

埃筛

埃拉托斯特尼筛法的缩写,EraSieve (这个英文其实是为了方便做函数名不要再写shake了)

它的核心思想其实是当确认了一个数是质数以后,把它的所有倍数打上标记说这玩意不是质数。那现在的问题就只剩下怎么确定一个数是不是质数了。

原理

我们先这样想,如果一个数 \(n\)​​ 是合数,那么一定可以表示为 \(a*b\)​​ , 其中 \(a、b\)​​ 都不能等于 \(1\)​​ 和 \(n\)​​ ,那这样的话 \(a、b\)​​ 都一定会是小于 \(n\)​​ 的,也就是说如果我们在\(2\sim n\)​​ 遍历的话,只要 \(n\)​​​ 是合数,那么它一定会在遍历到它之前被筛掉(相当于是它是某个小于它的数的倍数)。那么我们只需要从 \(2\)​​​ 开始遍历,记录每一个数有没有被筛掉,如果在遍历的它的时候没有被筛过,那么它就一定是质数(相当于是它不能表示为某个小于它的数的倍数,大于它的数就更不可能)。

代码
int not_pri[MX]={0};//not_pri[i]表示i这个数是否为质数,质数为0,非质数为1
void EraSieve(int range){//筛出所有小于range的质数
    not_pri[1]=0;//如果想让not_pri表示非合数,那么这里需要让not_pri[1]=1
	for(int i=2;i<=range;i++){
        if(not_pri[i]==0){//如果i不是质数,那么它就没有必要再翻倍,因为它的倍数必然可以表示为它的某个质因数的倍数,所以必定已经被筛过了
            for(int j=2;i*j<=range;j++){
                not_pri[i*j]=1;//筛除倍数
            }
        }
    }
}
复杂度分析

时间复杂度:\(O(n\log\log n)\)​ (范围*质数个数)

欧拉筛

也叫线性筛,EulerSieve (还是方便的函数名)

欧拉筛其实是针对埃筛的超级优化,它们的大致想法是一样的,都是把质数的倍数打上标记来筛掉合数,埃筛的花时间主要是在筛掉倍数,我们继续想一想埃筛的遍历方式里面是不是有针对合数的倍数的剪枝,但是我们来看这样一个例子:

\(i=2\)的时候,筛掉了\(\ \ 4,\ \ 6,\ \ 8,10,12,14,16,18\dots\)

\(i=3\)​的时候,筛掉了\(\ \ 6,\ \ 9,12,15,18,21,24,27\dots\)​​

你会发现,虽然我们只筛掉质数的倍数,但是在上面那个例子里,所有 \(6\)​ 的倍数都会被筛两遍,那如果一个数有多个质因子呢?重复筛的次数是不是更多了?是不是就造成了更多的无用循环?那么这个只根据是不是质数进行剪枝是不是可以更加精准呢?所以欧拉筛就诞生了。

让每一个数只被它最小的质因数筛到,就是欧拉筛改进后的剪枝方法,也是欧拉筛的核心思想。那具体要怎么实现呢?

原理

我们拿 \(12\) 做一个例子,按照我们埃筛的方式来,会这样反复的筛掉 \(12\) : \(2*6=3*4\) 。想办法只用\(2*6\)来筛掉 \(12\) 就可以了,我们可以发现一个数除以它最小的质因数得到的数一定是它最大的因数(\(2*6\)),而其他的表示方法一定可以分解成更小的质因数相乘的形式(\(3*4=3*2*2=2*6\)),也就是说如果一个数 \(n\) 除以一个质因数 \(x\) 得到的数 \(y\) 是一个比这个质数 \(x\) 更小的质数的倍数,那么这个质数 \(x\) 就一定不是最小的质数,自然也就不用这个质数来筛了。 结合 \(12\) 的例子来理解一下这句话:\(n=12,x=3\)\(y=12 /3=4\) ,因为\(\ 4=2*2\ , \ 2<3\),所以 \(3\)​ 不是最小质数。

其实这样麻烦的判别方法对于单个单个的数来说肯定不是最优的(直接挨个枚举\(2\sim\sqrt{n}\)​​看是不是整除岂不是更简单),之所以要用这样的一个性质,是因为我们需要筛啊,筛就需要枚举倍数,所以需要找一个通过倍数的关系来确定是不是最小质因数的性质。

接着看,可以发现只要是任何一个大于 \(2\) 的质数 \(x\) 来乘上 \(4\) 得到的准备筛掉的数一定可以写成 \(2*2x\)的形式,就是说它的最小质因数肯定就不是 \(x\) 了,而是 \(2\) 了,那么就没有必要继续将之后的质数都没有必要筛掉他们的 \(4\) 倍,因为他们的 \(4\) 倍数肯定可以被 \(2\) 筛掉。更一般化的形式就是,如果正在筛掉一个质数\(x\)\(y\)倍,而这个 \(y\) 能被一个比 \(x\) 小的质数 \(z\) 整除,所以 \(x*y\) 一定可以表示为 \(z*k\) ,而 \(z\) 是比 \(x\) 小的,回到欧拉筛的核心思想:“每一个数只被它最小的质因数筛到”,所以所有大于 \(x\) 的质数都不用筛 \(y\) 倍了,因为一定可以在筛去 \(z\)\(k\) 倍时筛去。(这里可以求出来\(\displaystyle k=\frac{x*y}{z}\));

为了用上这个那我们再改一下筛的方法,埃筛的方法是通过枚举一次性把这个质数的所有倍数全部标记完,而欧拉筛是先枚举质数的乘数,每一次标记上目前所有已知质数的某个确定的倍数(比如说都筛去目前已所有质数的\(3\)倍,下一次筛去所有已知质数的 \(4\) 倍,每筛一次单个质数最多只会多一倍),这样就可以利用到上两段发现的性质来进行优化了。而这里的枚举倍数再仔细一看,其实可以利用埃筛里枚举的 \(i\)\(i\)​ 在这里有了两个意义:①当前需要判断是不是质数的数;②所有质数筛去的倍数

代码
int pri[MX]={0};// pri[i]储存质数表,其中pri[0]表示这个质数表里有多少个数
int not_pri[MX]={0};//同埃筛里的not_pri[]
void EularSieve(int range){
    for(int i=2;i<range;i++){
        //i的第一个意义:判断i是不是质数,判断理由和埃筛一模一样
        if(not_pri[i]==0)
            pri[++pri[0]]=i;
        //i的第二个意义:筛去 目前所有已知质数*i 得到的合数
        for(int j=1;j<=pri[0];j++){//枚举当前已知质数
            if(pri[j]*i>range)break;//超出判断范围
            not_pri[pri[j]*i]=1;
 			//重点!
            if(i%pri[j]==0)break;
        	//用上面一般化的形式来表述的话,x=pri[j],y=i,这里由于pri是递增的,所以就相当于是判断y能不能被更小的质因数整除
            //如果i%pri[i]为0了,那么就找到了一个比x小的质数z,那么之后的倍数的最小的质因数就不再是x而是z了,所以不用接着筛了
        }
    }
}
复杂度分析

时间复杂度:\(O(n)\)​​​​​ (每个数只被筛去一次)

posted @ 2021-08-06 17:38  RagnaLP  阅读(105)  评论(0编辑  收藏  举报