日前,中科大软件学院二年级研究生 HCOONa 发表奇文 驳 GarbageMan 的《一个超复杂的简介递归》——对延迟计算的实验和思考。据他自称发此“文章”是为了“打”我“脸”,下面就来看看他是如何“打脸”的。
在C语言初学者代码中的常见错误与瑕疵(5)中,我剖析了一位初学者针对如下问题所写的代码,
在世博园某信息通信馆中,游客可利用手机等终端参与互动小游戏,与虚拟人物Kr. Kong 进行猜数比赛。
当屏幕出现一个整数X时,若你能比Kr. Kong更快的发出最接近它的素数答案,你将会获得一个意想不到的礼物。
例如:当屏幕出现22时,你的回答应是23;当屏幕出现8时,你的回答应是7;
若X本身是素数,则回答X;若最接近X的素数有两个时,则回答大于它的素数。
指出其中的一个主要问题是从X分别向前后两个方向查找素数的解决方案。因为其中的一个素数可能离X很近,另一个则离X很远。在这种情况下,去查找较远的素数是在做无用功。
我提出的改进办法是在X前后依照下面次序
X X+1 X-1 X+2 X-2 X+3……
交替寻找素数。这种方案在最坏情况下(X两边素数到X距离相等),与原方案计算量相同,但对于距离不等的情况则不会做无用功。
判断素数的方法为试除法,即用[2~X1/2]之间中所有的整数试除X,如余数都不为0则X为素数。
当X较大时,由于区间内素数个数远低于整数个数,所以,更优的解决方案是用[2~X1/2]之间中所有的素数试除。在一个超复杂的间接递归——C语言初学者代码中的常见错误与瑕疵(6)中我实现了这种方案。
由于这种方案过于精打细算,所以代码比较复杂。
对此,HCOONa发表评论:
这道题不是应该预先计算素数表,然后用二分查找吗?怎么会搞得这么复杂?
我考虑过你说的这种方案但我认为计算出直到大于等于X的素数表得不偿失举例来说 X=100要计算出直到101的素数表计算过程中大约100个数都需要试除2 3 5 7而文中的方案只需要对100 101这两个数分别用2 3 5 7 和 2 3 5 7 11试除一下就可以了这个差别太大了如果X=1000000000这个量级两者之间确实是天壤之别计算素数表还有一个问题就是事先不清楚到底有多少素数
因此很难估计数组大小
另外二分查找应该是用不上的
这就看你这个问题需要回答多少次了,
我只考虑回答一次的情形
一般说来,10^9 以内的素数并不多,用筛法的话很快就能得到一个素数表,接下来使用二分查找一下就能找到给定的数是不是素数,以及距它最近的两个素数是什么。
而你的方案则有可能需要更多的计算,比如说最近的素数差的比较多的时候,尤其是在 10^9 这个数量级,素数会变得比较稀疏,重复的计算可能会花费更多的时间。
如果你觉得维持一个素数表所需要的空间太大了,难以接受,那么我建议使用 Rabin-Miller 素数测试。
据我所知
Rabin-Miller 素数测试只是一个可靠性很高的近似测试
不能保证一定测试出素数
就本问题的要求来说
我认为不适合应用这种方法
或者
建议你抽空也写一个
或者写两个
比较一下
如何?
之前我假设这个问题是需要回答多次的,即给定若干个数字,分别回答这个问题,也是ACM中常见的情形。
原问题确实是需要回答多次
但我没按照那个写
因为我觉得那有些过于偷奸耍滑了
所以只按照回答一次写的
但即使是回答一次
也需要测试这个数周围几个数是否是素数
所以才有了这种方案
素数的个数和分布情况见,其中指出,10^9 以内的质数有 5761455 个,所占比率是 5.761455%
另外一个已知的结论是,素数会随着数的增大而变得越来越稀疏。
但我的方案只需要3000个左右的素数表
3000/5761455 = 0.052%
你的方法只是惰性计算质数表,本质上并没有什么提高,而且是用的比较原始的质数表生成方式,生成的效率比筛法要慢,粗劣的分析见这里。
你把用筛法生成质数表的成本给忽略了那可不是可以忽略不计的一项而且这不干稀疏什么事情理由我前面已经说了
除此之外,你是对给定数值附近的每一个数“由近及远”的分别进行判断,因此在质数已经非常稀疏的情况下,难免会计算很多数据。而如果已经有了质数表,则可以直接找到相邻的两个质数,无论它们距离给定的数值有多远。
你把用筛法生成质数表的成本给忽略了那可不是可以忽略不计的一项而且这不干稀疏什么事情理由我前面已经说了
毫无疑问,在生成同样大小的素数表的时候,筛法比这种直接的素数判断并生成的算法要快。我给的链接不知道为什么被吞掉了,这里 http://plussai.iteye.com/blog/1070387 有一个粗略的分析。即便是你的方法,把惰性生成素数表改成提前生成素数表,也是一个能够简化问题,提高效率的方法。
下面再说只问一次的情况,如果只问一次的妥妥的用 Rabin-Miller 素数测试。Rabin-Miller 素数测试可以以任意给定的精确度来判断一个数是不是质数,虽然本质上是非确定性的算法,但是如果你要求的精确度非常高,就可以近似认为给出的结果是正确的,比如说这个精确度比 10^-11 还高。
“就可以近似认为”你弄错了场合连问题的基本需求都不管不顾了这个问题根本没有说要求一个“近似”素数
具体要比较性能的话比较麻烦,我的想法是在给定的区间上均匀的生成 10000 个随机数,然后分别问你和我的两个程序,然后判断总的用时,我相信我的程序会比你的快 :P但是鉴于你说你考虑的情况是只问一次的情况,我就没做这个不公平的测试。如果使用 Rabin-Miller 素数测试的话,省却了打表的时间,对于 10^9 这个数量级,理论上会比你的算法快出好几条街。
所以其实我觉得你这个方案不好,增加了不必要的复杂性,还没有更好的解决问题。思路毫无疑问是正确的,但是延迟计算而不是预先计算这个策略选择的不太好,由此带来的问题就是代码非常复杂。再加上你标题写的是给初学者,我觉得就更应该注意一下,有点把问题搞复杂了。
Talk is cheap , show me your code.
Rabin-Miller 素数测试可以给出一个非常精确地答案,该答案的精度高于浮点型数据的精度时我看不出来什么理由不接受这个“近似”答案。更何况,整形计算也是有非常微小的几率发生错误的,这是由于CPU制程越来先进,而由量子力学理论所预言的。
越来越扯了
直接回答"非常精确地答案"是不是"精确"
连浮点数都上来
这个问题和浮点数半毛钱关系都没有
“整形计算也是有非常微小的几率发生错误的”
天哪!
你们科大就学这个?
老师怎么教的?
回去跟你们老师讲讲
看你们老师脸红不脸红?
那你是对我说的第一段话不信呢还是第二段话不信呢,还是都不信?我要是写了程序可就要发文章打你脸了。
如果楼主的数学和物理没有学好,不如再去读读书补习一下,没有必要泼脏水。
然而事实说了什么?
首先是自说自话,伪造前提条件:
该题有两种模式,一种是只考虑一次问答的情况,另一种是考虑连续问答。
但又不得不用虚伪的态度承认:(MD,似乎有点露怯啊。露怯还怎么将不要脸进行到底)
首先肯定一下,GarbageMan 的思路是正确的:在给定的 n 附近由近及远的进行素数判定,直到找到一个素数为止。
尼玛!昨天不是说什么“其实我觉得你这个方案不好,增加了不必要的复杂性,还没有更好的解决问题”吗?不是大言不惭什么“但是延迟计算而不是预先计算这个策略选择的不太好,由此带来的问题就是代码非常复杂。”吗 ?怎么开始自己打自己脸啦?
然后在伪造的前提下开始圆谎:
如果考虑多次问答的情况,考虑平均情况,延迟计算最终也会计算出大部分的素数表。与其动态的不断补充素数表,还不如用更有效率的方法直接计算出整张素数表,所以对于这种情况,GarbageMan 的优化是毫无意义的。
我的算法本来就是为一次问答设计的,对此他早就一清二楚。所以所谓“如果考虑多次问答的情况,考虑平均情况”“所以对于这种情况,GarbageMan 的优化是毫无意义的”难道不是屁话吗?
Rabin-Miller 素数测试,是一种素数判定法则,利用随机化算法判断一个数是合数还是可能是素数。
听起来像是一个不靠谱的算法,但是该算法可以以任意给定的准确率给出可能正 确的答案。当这个准确率足够大时,我们可以近似的认为这个算法给出的答案正确。(这一点遭到了 GarbageMan 的疯狂嘲讽,我猜他不知道为什么无穷大的倒数等于 0)
弱智想赢得辩论,就只能把对手说成弱智。
有关 Rabin-Miller 素数测试是否真的比通过试除法检验素数快,我们暂且将这一问题留待实验结果说明
嗯,分歧在于Rabin-Miller近似法是否适用于精确问题,但却用Rabin-Miller比试除法检验素数快来论证。尼玛!你这什么逻辑啊?猪都不会这么想!
GarbageMan 自鸣得意的一个优化就是延迟计算素数表。在我看来,这是一个完全没有必要的,并且极大地增加了代码复杂度的优化。在多次问答的情况下
看到了吗。偷来的锣鼓敲不得。偷换的前提,只好鬼鬼祟祟地猥琐地藏在后面扭扭捏捏小声地说。
我的建议是,使用初始化方法预先计算从 [2, log MAX_N] 之间的素数,然后再用 GarbageMan 的
get_nearest
方法进行计算。在问答量比较大时,这种方法甚至会比 GarbageMan 优化过的算法还要快。
这不但是卑鄙的掩饰,同时也是自供。等到您后面知道了“在问答量比较大时”里的“比较大”到底是多大时,您就明白了。
素数筛法已经十分先进了,甚至有亚线性时间复杂度的算法,因此,在实现生成素数表的情况下,没有理由不选择素数筛法。
用好听的“亚线性时间复杂度的算法”来蒙骗读者,因为一般人都会觉得线性时间复杂度很好。但对我的算法只有二分之一次方时间复杂度绝口不提。什么叫欺骗,这就是欺骗。选择性地告诉你一部分事实,同时选择性地故意隐瞒一部分事实。
我建议在计算素数表的时候,直接计算到比 n 的上限还要大的一个素数之后再停止生成素数表,然后通过二分查找,直接确定给定的 n 在素数表中的位置,从而找到距离 n 最近的素数。
不解释。这个二分法的愚蠢程度和陈良乔对链表使用二分法查找有得一拼。
算法的思路至此介绍完毕,下面将设计实验来验证我的想法是否正确。
嗯。伪造前提的铺垫已经完成,下面可以进入角色了。我不知道他在写这段话的时候有没有脸红。
实验分别比较在单次问答模式下,GarbageMan 所用算法和 Rabin-Miller 算法的用时;在多次问答模式下,GarbageMan 所用算法和其他算法的用时。其中,在多次问答模式下,需要对 GarbageMan 所用的算法进行一些微调,以保证测试的公平性和正确性:
尼玛。把我专为单次问答设计的算法进行肢解篡改,以便绑架到他自己亲口承认的“不公平的测试”中去,但还要假装“公平”。这等的虚伪和伪善需要几吨包天狗胆和寡廉鲜耻啊?!
编译选项
/STACK:10485760,1048576 /O2
轻描淡写地提了一下编译选项。这个编译选项很多小朋友不知道,因为几乎没人用过。这个选项是因为他自己的筛法计算109内的素数表需要巨大的内存开销。素数表对他自己的方案是有利因素,但对于我的方案来说,由于根本不需要这么大的内存,所以不但没好处,反而有坏处。因为访问大块内存是有时间开销的。我相信,微软如果知道这个编译选项会被无耻之徒这样利用,一定会对设立了这个选项而后悔不迭。微软,你欠我一个道歉。
给出的测试代码依赖于 C++11 标准中提供的随机数生成函数,因此只能在 Visual Studio 2013 和较新版本的 g++,clang++ 上不需要修改的通过编译。使用较低版本的编译器编译时,可以结合 boost 库提供的支持,进行有限的修改后通过编译。 如果使用 g++ 或者 clang++ 进行编译的话,请使用参数
-O2 -std=c++11
。
大家看到了吗?结果依赖于随机数生成函数,很多编译器的随机数生成函数都有毛病,这个毛病就是不那么“随机”。这里他留了一个伏笔,以后有人发现作弊,他可以赖到C++11 标准中提供的随机数生成函数上去。另一个作用就是是暗补他前面说过的“均匀的生成”“随机数”的谎话。除此之外,把我的C代码当作C++代码编译,“结合 boost 库”,这都是在不动声色地作弊。这就是他口口声声的“公平”。
实验结果
当设定 n 的范围在 [1, 10^6 - 10],且生成 50,000 个随机数时,多次问答模式下的测试结果如下:
Elapsed : 3900005ms // GarbageMan 的方法 Elapsed : 500001ms // 积极计算的方法 Elapsed : 2890109ms // Rabin-Miller 算法 Elapsed : 270015ms // 素数筛法和二分查找
再测一次:
Elapsed : 3920017ms // GarbageMan 的方法 Elapsed : 500001ms // 积极计算的方法 Elapsed : 2800004ms // Rabin-Miller 算法 Elapsed : 300001ms // 素数筛法和二分查找
由大数定律可知,GarbageMan 的算法在平均情况下运行效率较低,并且低于积极计算的方法,可见不仅白优化了,还起到了负面效果。
由于实验结果显示,在多次问答模式下 GarbageMan 的算法运行效率仍然低于为单次问答模式所设计的 Rabin-Miller 算法,因此没有必要再进行单次问答模式的实验。
明眼人一下就能看出这段的毛病,但许多见识不多的小朋友可能看不出来。注意到“在多次问答模式下 GarbageMan 的算法运行效率仍然低于为单次问答模式所设计的 Rabin-Miller 算法”这句话的玄机了吗?在“多次问答模式下”,无论如何我的算法都是吃亏的,因为我的算法本来就是为单次问答设计的。“多次问答模式下”会把我的算法的优势彻底铲除。但是Rabin-Miller 算法不受单次模式或多次模式的影响。他刻意造了这个生涩难懂的句子,以回避公布单次问答模式下我的算和Rabin-Miller 算法的结果。然后用刻意营造出的结果,以及八竿子打不着的不相关的用来蒙人的“大数定律”,以便装腔作势地宣布他精心炮制的谎言。
前面他曾信誓旦旦煞有介事地表示,他要“分别比较在单次问答模式下”,但要宣布结果时,却硬生生地把这话自己吃了下去。无耻至斯,叹为观止!
注意到这段开头的那个“ 50,000”了吗?还记得前面提到过的他在回复中说的那个“ 10000 个随机数,然后分别问你和我的两个程序,然后判断总的用时,我相信我的程序会比你的快 ”“会比你的算法快出好几条街”吗?为什么他要把“10000”个随机数改成“50,000”呢?原因很简单,生成一个109之内的素数表需要很多时间成本,为了“证明”他的方法快而又想用“事实”来编造谎言,这个巨大的时间成本就需要被分摊。这就是原因,没有之一。
后面的结论就不引述了,谁愿意相信谁相信!这个世界上一向不乏乐于相信谎言的傻B!
回过头来看这篇奇文标题——驳 GarbageMan 的《一个超复杂的简介递归》——对延迟计算的实验和思考。我们不禁要问,他到底驳了什么了?
我认为Rabin-Miller 算法不适合精确计算,他驳了吗?他吭哧瘪肚地用移花接木的手法刻意地营造了一个实验,我就算是退一万步,抛开他的种种龌蹉的作弊作假不提,就算假定他的结论是对的,那也只说明了Rabin-Miller 算法比我的快而已,那是对“Rabin-Miller 算法不适合精确计算”这个观点的驳斥吗?
从这里不难发现,中科大的这个HCOONa的科学素质极低,基本上是一个逻辑文盲。他的奇文中这种低素质不自觉的流露到处都是,比如标题中竟然明晃晃地出现了两个错别大字“简介”,经验告诉我,优秀的程序员对错别字非常敏感,一般不会写错别字。再比如素数的定义:
素数,又称质数,指除了 1 和该整数自身外,无法被其他正整数整除的正整数。
尽管他装模作样的在参考资料部分引用了
但维基百科中对素数的定义全然不是这么回事。按他的定义,1也是素数,这和谭浩强是同一个水平。一个认真的小学生都能发现他的这个错误。所以科学素质其实跟学历没有必然关系,有很多高学历者的科学素质并不高,有的甚至是自觉或不自觉的骗子。比如,清华大学的几朵奇葩教授,……以后有机会再跟大家聊。
我认为计算109量级的素数表,哪怕是用简单的筛法,也必然要付出高昂的时间成本和内存成本作为代价。他驳了吗?他只是用精心构建的实验去掩盖而已,而且掩盖的手法实在不咋地,以至于处处露马脚。即便抛开这个人极其龌蹉低下的技术品德不谈,作为一个骗子,他也是一点技术含量都没有。被扒光内裤之后,你看到的无非就是一个无知无耻之徒在裸奔而已。
曾经有人说过,世界上有三种谎言:谎言,弥天大谎,统计局公布的数字。现在我们有幸见识了第四种,就是HCOONa创造的这种用似是而非的理论,精心炮制的实验,无耻的伪公平架势,选择性的事实构筑的谎言。
为什么他要炮制这些谎言欺骗大家,原因只可能有一个,前一天晚上在我的博文评论中放了半宿不通的狗屁,导致肚子里憋了两大泡稀屎:
在此也劝告 GarbageMan,没什么本事就别在那叫嚣了,还写什么《C语言初学者代码中的常见错误与瑕疵》,误人子弟。
GarbageMan 多次对我进行人身攻击,并且侮辱我的母校。在此我要说一句,GarbageMan 你真是人如其名——渣男,人品渣,技术也渣。
就这样一直忍到第二天中午,实在憋不住了,没有手纸找不到厕所也顾不得了,只好~~~~喷吧!
好臭!
终于彻底地应验了鲁迅老先生的那句话:“始于作伪,终于无耻!”。