詹氏筛法和詹欧筛法——撵爆线筛的强大筛法,算法史的骄傲

先介绍一下伟大的发明人:
ZZX,就读于广东省中山市纪念中学。
曾获得多个奖项,是一个超强的OI选手。
发明了两个改变世界的重要算法:詹筛詹增


先说以前的筛法是发么打的。
一开始,我们打的都是埃氏筛法。就是从前往后扫,遇到一个素数,就筛去它的倍数,打上标记。没有打上标记的就是素数。
这个算法自然是很慢的,据说时间复杂度是O(nlnn)O(n \ln n)
于是我们就引进了欧拉筛法,也就是线筛,那么我们可以保证对于每个数,它们至多被筛一次,所以时间复杂度是O(n)O(n)的。
线筛比前面的算法优秀很多,所以一般情况下,我们都会选择打线筛。
可是,到此为止了吗?
不,我们还有詹筛


其实詹筛的时间复杂度并不优秀,它不是线性的。
本质上它就是埃氏筛法的优化版本。
n=108n=10^8的时候,在JZOJ上评测,詹筛比线筛快了大概50ms。
如果这个nn可以大些,詹筛或许不如线筛,可是,你还能再大多少吗?在OI比赛当中,你的空间有多少,你的时间有多少?所以在OI比赛的时间和空间范围限制中,詹筛是撵爆线筛的。


詹筛的思想其实很简单,就是在埃氏筛法当中,把22给拎出来单独考虑。
显然除了22以外,其它的偶数都不是合数。在埃氏筛法的处理当中,与22有关的东西其实很多。
于是就有了下面这个代码:

	for (int i=3;i*i<=n;i+=2)
		if (!inp[i])
			for (int j=i*i;j<=n;j+=i<<1)
				inp[j]=1;

是不是特别短?
这样我们就可以将标记数组给求出来,当然,这不包括偶数,在素数判定的时候我们要特判一下就好了。
詹筛是不需要将素数表打出来的,如果要打出来,扫一遍就好了。
或许不会比线筛优化多少,但是詹筛的代码太简洁了,看起来比线筛舒服很多。


欧拉:我不服!量你个毛头小子,居然敢撵爆欧拉筛法。
ZZX:行行行,我们结合起来好不好?
于是有了下面的代码:

	p[*p=1]=2;
	for (int i=3;i<=n;i+=2){
		if (!inp[i])
			p[++*p]=i;
		for (int j=2;j<=*p && i*p[j]<=n;++j){
			inp[i*p[j]]=1;
			if (!(i%p[j]))
				break;
		}
	}

这样就比线筛和原来的詹筛快多了哈哈哈……
于是我们起名叫詹欧筛法
欧拉笑嘻嘻地回到他的天堂去了,原先的詹筛就算是吸氧气都比不上詹欧筛法(处理素数表的情况下)!


有一句话说得好:优化永无止境!
所以说还可以再优化吗?
吸一口臭氧
不只是考虑22,也考虑33
这似乎是一种不错的思路,但代码复杂度一定大很多,况且计算机是二进制的,用33可能会造成一些不好的常数影响。

其实有一种比较好的优化:
我们只需要存奇数,偶数的标记数组是可以不需要的。
那么就变成这样:

	p[*p=1]=2;
	for (int i=3;i<=n;i+=2){
		if (!inp[i>>1])
			p[++*p]=i;
		for (int j=2;j<=*p && i*p[j]<=n;++j){
			inp[i*p[j]>>1]=1;
			if (!(i%p[j]))
				break;
		}
	}

这样可以将标记数组的空间减半,并且时间上优化约40ms。


不过在有些时候我们利用筛法求的东西不只是素数,可能还有什么μ\muϕ\phi之类的。
下面是求最小质因数的例子。

	mp[2]=2,p[*p=1]=2;
	for (int i=3;i<=n;i+=2){
		if (!mp[i])
			mp[i]=i,p[++*p]=i;
		for (int j=2;j<=mp[i] && i*p[j]<=n;++j)
			mp[i*p[j]]=j;
	}

这个程序比上面的周筛法(连读)慢,但是它可以求出最小的质因数。
至于其它的……那就不在这里说明了。

posted @ 2019-02-25 18:46  jz_597  阅读(299)  评论(0编辑  收藏  举报