笔者按:
该文作于2004年10月,原文发在http://addone.blogchina.com。当时我已经大四了,很遗憾没有再来一次的机会。
近日,有幸能够代表学校参加了第26届ACM/ICPC国际大学生程序设计大赛上海交大中国赛区预选赛网络选拔赛。作为唯一一支校队,我们尽管得到了许多
帮助和支持,但由于前期准备和估计不足,我们最后还是失利了。不过尽管如此,我们仍然很骄傲,因为我们做对了一道题。尽管我们只做对了一题,我们仍然很骄
傲,因为我们从中学会了很多,很多。
比赛从13日开始,连续进行3天的热身赛,然后休息1天,在17日正式比赛。当我们得到这个消息的时候,正是13日的下午,而就在两个小时前,参赛的名单
还没有最后敲定。
当天晚上,我们开始安装比赛规定的系统,遗憾的是,竟然没有能够找到可以安装的Red
Hat Linux
9.0,于是,只好把我自己的使用SuSE Linux
9.1(使用GCC3.3.3,比赛要求编译器GCC>=3.2.2)的电脑给用上了。
连续三天的热身赛中,我们没有完成任何一道题。但是我们确实没有被那些题目难住,我们轻松就可以完成许多题目,只是不知为何,提交结果总是超时。到了最后
一天,我们一个几乎可说是必对的答案被判为输出格式错误。在这三天里(以及之前的日子),我们没有得到任何来自数学老师、算法老师的帮助,完全依靠的是过
去的编程经验。我们思考着过去的题目,百思不得其解:为什么我们的结果不能通过评判系统呢?
比赛的前一天,由于大家的强烈要求,组委会公布了第一次热身赛的数据格式及参考程序。当我们看到参考程序时,我们震撼了。
几乎是在看到那些优美的代码的同时,我们几个都产生了同样的想法:在我们过去的日子里,写出的那些代码与此相比,简直是......垃圾。而评判程序采用
的输入数据不管是规模上还是数量上,都要远远超出Sample
Input给我们的印象(当然,不会超出题目指定的范围)。也就是说,一个限时5秒的程序,如果在本地运行,在0.5s内还不能完成的话,就很难有希望
了。我们可以看到,如果采用一般的简单方法去解决那些题目,几乎都没办法在给定的时间限制内给出答案。也就是说,这类题目都具有能够用数学的方法进行算法
加速的方法。部分题目更是采用了"空间换时间"的方法,极大的加快了程序的执行速度。源代码的优美和高效令人震撼,其谨慎程度更是匪夷所思。
我们这才明白,我们之前根本就没有理解这些题目,没有理解这个比赛。
在正式比赛中,尽管有指导老师在场,我们依然没有能够创造奇迹。但尽管我们没有接受过任何的专门培训,我们依然凭借"空间换时间"的方法,完美的完成了一
道题。而另外的3道题目,也在比赛结束后很快被我们逐一攻破了。尽管没有能够通过网络选拔,但我们从中获得的经验,对我们将来的程序设计,已经产生了巨大
的影响:
1.空间换时间。这是我们从参考程序中学来,并在最后应用得最活的一招。现代的计算机CPU的速度增长已经显著慢于内存,而人们对速度的要求也在不断增
长。如何才能适应这种新的变化呢?在过去小内存时代提倡的"时间换空间"的编程思维在今天已经不再适用了,如果头脑依然受着传统思维的影响,是没有办法开
发出高效的程序的。在比赛中,对程序的时间限制比较严格,通常限定为1s-5s,且计算量相对比较大。但与此同时,空间的限制则宽松得多,通常只要是
64MB以下即可。如果用常规的方法来编写程序,一来很难达到题目的时间要求,二来白白浪费了大量的可用空间。
对于一些递归方式的题目,如果采用常规的递归方法,往往根本无法在有限的时间内解决。这类题目大多可以依赖于数学方法来把递推关系转变为简单的表达式,或
者能够使用一些特殊算法进行加速。但如果题目给定的数字范围在规定的空间限制以内,则往往可以通过"以空间换时间"的思维,用极其简单的算法,大大的提高
程序运行的效率。首先可以使用从小到大的循环来得到一个保存有各项结果的大型数组,其中需要递归的地方,可以直接从数组中读取前面已经计算完成的结果,而
不必再次进行计算。这样的过程通常不会太慢(因为使用了查表来模拟递归),我们的一个302万个元素的数组全部计算完成耗时不超过0.1s。而需要求值的
时候,只需直接查表即可,这段时间几乎可以忽略不计。
我们在部分题目中应用了这种思维方式,取得了非常不错的效果。在一个题目中(时间限制1s),我们使用了4个大型数组,每个数组都含有365万个元素,4
个数组全部计算完成仅耗时约0.5s。尽管代价是巨大的空间消耗,但是4个含365万个元素的带符号int数组所耗空间为:3650000*4*4=
58560000(字节),即约58MB,远没有达到题目的64MB内存的空间限制,完全可行。而后期的取值计算,则完全变成了查表求值,就几乎不消耗时
间了。
2.数学思维。数学思维并不像不少人想象的那样可有可无。事实上,数学思维对于程序设计来说极其重要,以至于完全构成了基石,注意到它的人就少了。包括算
法设计、微积分、代数学、几何学在内,数学构成了整个程序世界的基础。它是如此完善,完善得让在其上构筑软件的人往往感觉不到它的存在。然而,当需要操作
底层世界时,当需要变动基石时,我们就会发现:正是它们充斥着一切。
也许这种说法有失偏颇。但不管在比赛中还是工作上,我们往往会需要一些快速、高效的代码,这些代码,都是经过数学方法对问题提炼后写成的,通常包含了大量
的建模思想以及加速算法。我们在发现程序运算效率无法满足要求时,往往会要求更快、更高效的算法和更好的模型。
这次比赛的几乎所有题目都涉及到了各种各样的算法问题。而在正式比赛的8道题中,则有5道可以归为纯数学问题。这些问题,虽然没有直接和常见的软硬件项目
相关,但却能利用其中的思维方法来提高现行软硬件系统的执行效率。这样看,数学的重要性不言而喻。
3.注重效率。代码的执行效率其实是相当重要的,只不过往往在软件工程中,为了保证代码的易读、易改,往往刻意的降低了写高效代码的地位。然而,在核心代
码中,如果能够合理的采用一些高效的代码,哪怕效率提升很小,在数据量非常大的时候(你往往无法准确估计,或者通常会低估这个值),效率的提高也是极其巨
大的。
在我们看到的参考程序中,对这类高效代码的重视程度已经相当高了,完全可以称得上是:分秒必争。我们可以在许多地方看到,使用位运算来代替常规运算来加速
运行;利用数组来模拟各种常用的数据结构;使用循环来代替递归等等。这些示例代码,许多都堪称经典,我们可以从中学会相当多的东西。
4.团队精神。在每一个参赛队伍中,都有3名队员及一名教练。队员之间的相互配合是相当重要的。而每名队员的特长、分工也相当有讲究。特别是由于题目全部
是英文,那么除了大家都应该有一定的英文水平外,还往往需要有一名队员在这方面比较突出。而作为基础的算法方面,则需要有两人来配合完成。至于编码调试,
由于满足比赛要求的代码大都短小精悍,一般有一名较有经验的程序员担任即可。这些分工、合作,说起来容易,要真正做到,可是相当不简单的,往往需要几个人
长期配合才有可能做到。
比赛结束了。失去了这次出线的机会,笔者已经没有机会重来了。只能期待后来人能够以此为鉴,多加修习,不要再过早的落入世俗的软件开发圈里了(对于那些有
志于从事计算机核心技术研究的人而言)。
有关比赛的资料,可以从
GOOGLE上搜索"ACM"或者"ICPC"来得到。