近期要实现的数据结构与算法
从未实现过的:
Dinic算法(网络流):NOI Profit等。
Stoer-Wagner算法(最小割):UVA 10989。
Trie图(字符串自动机):SPOJ WPUZZLES、POI #7 Virus、Ural 1158、Ural 1269。
块状链表(链式数据结构):NOI的题。
以前实现过,但是需要活用的:
线段树的题目:PKU 3225。
平衡树的题目:NOI的题。
6月份的前两周把它们做完。
从未实现过的:
Dinic算法(网络流):NOI Profit等。
Stoer-Wagner算法(最小割):UVA 10989。
Trie图(字符串自动机):SPOJ WPUZZLES、POI #7 Virus、Ural 1158、Ural 1269。
块状链表(链式数据结构):NOI的题。
以前实现过,但是需要活用的:
线段树的题目:PKU 3225。
平衡树的题目:NOI的题。
6月份的前两周把它们做完。
本列表收集算法资料丰富的网站,目前仅收英文网站。排名不分先后。
http://www.nist.gov/dads/ Dictionary of Algorithms and Data Structures,基本上各种数据结构和算法都会有介绍,个别的介绍太简短,没提供什么有用信息,大多数词条还是蛮有价值的,特别是有些More Information里面的链接。
http://www.research.att.com/~njas/sequences/ 在线数列百科全书。我只能说……谁用谁知道。
http://compgeom.cs.uiuc.edu/~jeffe/ Jeff Erickson教授的个人主页。这个教授真是一个好人,把自己的课程的一切资料——讲义、试题——放了上去。比如说那份详实的Algorithms Course Materials就是一份不可多得的好资料。
http://www.csse.monash.edu.au/~lloyd/ Lloyd Alison教授的个人主页。我发现里面关于某些东西(例如Suffix Tree)的介绍很不错,还有Algorithms->2D->Life里面的东西你一定要看看。
http://www.introductiontoalgorithms.com/ 一个关于Introduction to Algorithms这本书的网站,上面有用的资料不比原书中多。不过若你没有这本书,只是想对于某个算法找到其定义或者伪代码,这个网站还是有用的。
http://www.cs.fiu.edu/~weiss/ Mark Allen Weiss教授——Data Structures and Algorithm Analysis in C/C++/Java 等书的作者——的个人主页。这里有这些书的源代码,我认为这些代码是很好的。
http://www.cs.kent.edu/~rmuhamma/ Rashid Bin Muhammad教授的主页,里面的Algorithms页面挺有料。
http://courses.csail.mit.edu/ 一份MIT的课程列表,其中的6.851很神奇。
http://www.math.ucla.edu/~tom/ Thomas S. Ferguson教授的个人主页,我在上面下载了一份不错的Game Theory讲义。
哦……这个列表目前很短,因为我很菜,知道的东西还很少。如果你知道包含好的算法资料的网站,请一定要留言告诉我哦!
第23章
调试本身并不是改进代码质量的方法,可以认为它只是一种补救措施、不得已的手段。在OI中应该尽量避免调试,应该在一开始避免错误的发生。调试的效率在人与人之间有巨大差距,所以培养调试能力十分有必要。
寻找缺陷和理解缺陷是调试中最费时的步骤。事实上在这点上不应该吝惜时间,一定要确保已经完全理解缺陷后再开始动手修改。
把需要尝试的事情在纸上逐条列出,这也许是一个加速调试得好途径,因为调试时的人大多都昏头昏脑的,最好有一个能让你保持清醒的东西。
优先检查最近修改过的代码。尽量对程序保持全局理解。永远不要随机地、尝试地修改代码,每一处代码改动都需要有明确的理由。一次只做一个改动。
心理因素让人看到他“希望”看到的东西,这对调试的影响有点大,所以要保持优秀且一致的编程习惯,要让变量名之间的“心理距离”尽量远。
把编译器警告级别设为最高,这是默认的。Profiler也可以当作调试工具,有时从不正常的运行时间或调用次数中可以看出错误。
第24章
一帆风顺的软件项目是神话,实际中的代码是经常需要经受剧烈变化的。好在实际中引起变化的(最?)重要原因——需求的变化——在我们的OI中不会出现,只要你别读错题了。
软件演化的基本准则是,演化应当提升程序的内在质量。
重构的定义是“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”。需要重构的理由在OI中同样充分的有:代码重复,冗长的子程序,嵌套过深,过长的参数列表,命名不当。
特定的重构方法很多,也很系统,但在OI中还是应该尽量在一开始就写出更好的代码,毕竟你没有太多时间。
如果确定要对代码——特别是正确(指不可能WA)的代码——进行某种大幅度的修改(不一定是重构),一定先备份一下。
第25章
性能调整在OI中也许没有你一开始想象得那么重要,似乎复杂度才更王道。任何情况下“性能”都不是头等大事,正确性才是。
程序需求方面,就是可能不切实际的复杂度分析,少数时候实际运行时间无法用理论复杂度衡量。程序的设计就是基本的算法了,如果这里面出了致命问题,再多的代码调整也无济于事。最后的手段才是代码调整。
代码调整的问题在于,高效的代码并不一定就是“更好”的代码。80/20法则是一定要注意的,Profile一下嘛……“随时随地”的优化是绝对不可取的。
常见操作的相对效率那个表真是好东西。它说我们整数的赋值、整数的加减乘、浮点数的加减乘、调用一个函数、用下标访问数组等操作所需的时间都相差无几!这还真是惊人的结论……(有空还是自己验证一下。)
优化前,一定要保存代码的可运行版本。
第26章
OI中可以考虑的代码调整技术:用查询表替代复杂表达式(没用过呢还)、使用惰性求值(常用的思想);展开(这个很有点意思)、哨兵值(这个我比较喜欢)、把最忙的循环放在最里层(可能被忽视的常识)、削减强度(值得注意);尽量减少数组引用、使用缓存机制(事实上很高级的东西,值得简单地尝试);利用代数恒等式(如果真的有而你没发现的话……)、削弱运算强度(常用的)、使用正确的常量类型(以前没有注意过,也许真的很重要)、删除公共子表达式(为了程序清晰也应该做);把子程序写成内联(“现代”计算机说这样做不一定就会性能提升)。
第19章
在表明bool值的时候,应该采用true和false而非1或0。如果从“理论”的方面论述这一点,可以说1和0是“神秘数”而true和false就不是。
比较长的判断语句中的bool表达式是一个容易出错的地方。在bool表达式方面降低复杂度的方法有:把它提炼到一个函数里然后忘掉它、试图用DeMorgan律简化它、用括号使它更清晰。
要理解很多语言中对于&&和||运算都有“短路求值”的处理,如果想避免它可以使用&和|。
按照数轴的顺序编写数值表达式,也就是说写MIN<i&&i<MAX而非i>MIN&&i<MAX。
在写复合语句时先写好开头和末尾的大括号,再填充内容。一种良好的习惯是将if、for后面跟的一条语句也放到一个单独的块里。
在程序中要避免三四层以上的深层嵌套,因为他们带来可能不必要的思维复杂度。可以考虑的解决方法有:重复判断一部分条件(违反DRY原则,故似乎不大可取);重构某些代码到子程序中(这个比较好);使用break语句的小trick;试图转化为一组if-else-if语句。
结构化编程就是仅仅是用顺序、选择、迭代三种控制结构的编程方法学。控制流是复杂度来源的很大一个方面。度量(思维)复杂度有一种简单的方法,但在OI中大多时候是没必要这样度量的。
第20章
软件质量的特性中有很多是与OI无关的,外在特征中也就正确性和效率需要注意,内在特征几乎都不用过多考虑。软件工程学之所以存在,很大程度上就是因为这些特征是互相矛盾的。
有时靠阅读和检查代码发现错误会比测试高效得多,极限编程(XP)的结对编程和代码检查是降低出错率的重要手段。在OI中不妨借鉴这一点,当发现错误时,先不忙着调试,先把代码看一遍。但是这样做的前提似乎是代码风格应该足够优秀,能被轻松地理解,甚至能被速读。
第21章
协同构建这部分内容,或许ACM/ICPC选手可以从中得到启迪,但对于目前的OI还是一点用途都没有。
第22章
测试在OI中最主要的体现是自己编些数据来测试正确性,这比较接近于集成测试。但与单元测试的思想类似的方法似乎也是应该应用的,也就是在编写完一个感觉上很易错的模块时应该马上独立地测试它。
第12章
有向图就是边是顶点的有序对的图,也就是说每个边都有起始顶点和终止顶点。任意两顶点间有且仅有一条边的有向图叫竞赛图。若任两顶点间都可以沿有向路径互达,图就是强连通的。
无向图的强连通定向就是把每条边指定一个方向,然后保证得到的有向图是强连通的。强连通定向存在当且仅当原图中没有桥。构造出这一强连通定向事实上很简单:按照一遍DFS的访问方向给边定向就可以了(书中的算法一如既往地麻烦)。
核心分配问题是这样的:n个商人,每人有1个商品,每人对所有n个商品按照自己的喜好进行排序。求一个把n个商品分配给n个人的方案,使不存在几个商人的集合,他们把分到的商品再次进行交换后可以使每个人都更满意。算法是:每个商人对自己最喜欢的商品的主人连一个有向边,找一个圈(由于每个点都有出边,所以这肯定是能找到的),按照这个圈进行分配,把已分配的顶点删掉,所有指向这些顶点的边重新计算。
网络就是存在源点s和汇点t的有向图,其中每条边都有一个可以认为是“容量”的权值。网络中的流定义为加在每条边上的一个函数,也就是说给每条边指定一个“流量”。合法的流应该满足:每条边的流量不超过其容量,除源点和汇点外,点的出边的容量和与其入边的流量和相等。流的净流量就是源的出边的流量和或汇的入边的流量和。求一个合法的净流量最大的流就是最大流问题。
网络的割是一组边的集合,满足把这些边从原图中删去后从源点到汇点不再连通。最小割就是边的权值(就是上述“容量”)和最小的割。最大流最小割定理的内容就是:网络中的最大流的值等于最小割的值。
求解最大流问题和最小割问题可以采用残量网络增广路的方法。这在很多很多资料中都有很好的介绍。《组合数学》这书似乎并不是学网络流的好教材。
第13章
χ(G)是图G的色数,就是说给图的每个顶点赋一个颜色使所有边的两个端点的颜色不同,需要的最少颜色数目。它也可以理解成将顶点划分成若干个集合,使每个集合的导出子图都是零图的最少集合数目。求图的色数很困难。
关于图G的色多项式pG(k),也就是说将图G用k种颜色的方法数。有一个结论:设T是一棵n阶树,则pT(k)=k(k-1)^(n-1)。它的证明也简单:第一个顶点有k种着色方式,以后每添加一个与当前顶点相邻的顶点都k-1种着色方式(因为它仅与一个顶点相邻),根据乘法原理得证。另外,n个顶点的零图的色多项式是k^n。求色多项式的一种方法是:当G不为零图时,设x、y是一对邻接顶点,设G1为原图去掉边x、y的图,G2为原图将x、y合并成一个顶点的图(原来与x或y相邻的顶点均与新顶点相邻),那么就有pG(k) = pG1(k) – pG2(k),递归求解即可。这个算法的正确性还比较好理解的,不过显然它是指数级的算法。它可以采用非递归的方法实现,就是维护一个图的集合,每次取出一个非零图的元素把它分成两个元素。
平面图就是可以“画”在平面上而且边不交叉的图。(更“数学”的定义似乎书上没有。)它有一些很好的性质:设一个连通的平面图的平面表示有n个顶点点、e条边曲线、r个所围区域,则:n+r-e=2。证明的方法是先对树证明,再往树上加边,保持等式始终成立。一个平面图必存在一个顶点,它的度至多是5,这是欧拉公式的推论。
五色定理是:一个平面图的色数至多是5。这个可以比较简单地证明,主要根据上面的欧拉公式的推论来利用那个特殊的顶点做突破口。它的加强版本四色定理也是成立的。
图的独立集的定义是一个顶点的集合,它的导出子图是零图。图的独立数α(G)就是图中最大的独立集的大小。求图的独立数很难。图的控制集是一个顶点的集合,它满足所有不在集合中的顶点都与集合中的点有边。最小的控制集的大小称为图的独立数dom(G)。图的团就是一个顶点的集合,它的导出子图是完全图。图中的一个团是图的补图的独立集,反之亦然。图的团数ω (G)是最大的团的大小。将图的点集划分成最少的团的数目称为团划分数θ(G)。完美图满足它的任何一个导出子图有χ(G)=ω (G)且α(G)=θ(G)。
图的弦就是连接圈的两个顶点且不属于该圈的一条边。如果每一个长度大于3的圈都有弦,图就被称为弦图。所有的弦图都是完美的。所有的区间图(定义不说了)都是弦图。
以上定理的证明均略,因为第一我也搞不太懂,第二它们在OI中似乎没什么用。
图的顶点连通度κ(G)是最少删去多少个顶点可以使图不再连通,定义κ(K_n)=n-1。边连通度λ(G)是最少删去多少条边可以使图不再连通。再设δ(G)表示图中顶点的最大度,则有显然的不等式κ(G)≤λ(G)≤δ(G)。
顶点连通度κ(G)不小于k的图称为k-连通图,一个图是2-连通图与以下结论等价:没有关节点;任两个顶点a、b都存在一个包含a、b的圈;每三个顶点a、b、c,都存在一条连通a、b且不经过c的路径。这些都很好证明。
关于图的点连通度的Menger定理的内容是这样的:设k是一个正整数,G是一个阶为n≥k+1的图,则G是k-连通的当且仅当对任意两个不同的顶点a和b都存在连接a、b的k条路径使得每对路径只有a和b两个公共点。
第14章
直线型代码有两种,一种是语句之间有(可能是极隐蔽的)前后依赖关系,另一种是顺序无关的语句。
对于依赖关系的语句,应该设法让这种依赖关系表现得极为明显,比如说第二个函数调用的参数是第一个函数调用的返回值,或者说它们有共同的(按引用传递的)参数。当然,也可以用数据来表明没有依赖关系的语句。
至于顺序无关的语句,也应该尽量让相关(操作于同一对象)的语句放在一起。也就是说,应该让越靠近的语句关系越大。
第15章
if语句的一种常见用途就是区分正常情况与异常情况。应该把正常情况放到if前面然后异常情况用else处理,这是一种好的习惯。但我认为如果所有的代码逻辑都是异常情况用if,似乎也不会带来太大困扰。特别是在遇到异常情况就会return的情况,先处理了所有异常情况,然后在“净室”的环境下写正常情况的代码,减少了缩进层次,似乎也会更清晰。这是我的编程习惯。
if-then-else语句串的排列一般采用常见的情况在前的方式来处理,这样可以(很不明显的)提高效率。case语句是类似的。提倡尽量在每一case语句后都break,否则很容易令人困惑。
第16章
循环有好多种,不过在OI中选择哪一种似乎是自明的。应该尽量避免在循环体内外重复语句,虽然有时候看上去是不可避免的。
循环的控制语句和循环的内部最好应该可以互相看作黑盒,完全分离。
循环的初始化代码应该紧紧放在循环前面。当这种代码进有一句时,把它放到for的第一个语句似乎是更好的选择。
尽管C中的for很强大,但是一般应该让循环头中的语句仅与循环控制相关。其实还是循环的控制和循环的内容分离。
使用continue可以避免一个能让整个循环体都缩进的if判断。
减少off-by-one错误的好方法是在脑海或者用纸笔模拟,对于优秀的程序员来说这不应该太费时。
我觉得在OI中采取从内而外编写循环的方法学不是个好主意。
第17章
如果能增强可读性,那么就使用return。
对于goto,我的观点是:永远不要用。事实上我坐到了这一点。
第18章
表驱动法是一个好主意,应该了解。其实OIers对于这种东西应该比一般的“程序员”熟些,大部分情况下,不就是一预处理么,呵呵。
第9章
这一章讲二分图中的匹配。给出了几个能启发思路的转化成二分匹配的模型:有禁止的棋盘上放非攻击车;有禁止的棋盘上放1×2骨牌;m个具有一定资格的人申请n个工作。将问题转化为二分图匹配模型的关键点是构造二分图,使可行解与合法的匹配一一对应,这样最优解一般就对应了最大匹配。
定义二分图的交错路径是这样一种路径:含有奇数条边,其中仅有第1条、第3条、……、最后一条边都没有包含在当前的匹配方案里,仅有路径的两个端点没有被匹配。一个匹配是最大匹配当且仅当不存在交错路径。求最大匹配的方法就是不断寻找交错路径并将使用其中的奇数边重建当前匹配方案的,这就是Hungary算法。书中给出的伪代码垃圾了,本来不用那么麻烦的,更好的代码参见我以前关于Hungary算法的文章
二分图的最小覆盖的意思是这样的:选定一些点,让所有的边都有端点被选择,选的点数尽量少。König定理是这样的:二分图的最大匹配数等于最小覆盖数。这个定理可以帮助解决另一类可以转化为二分图匹配模型(先转化为最小覆盖模型)的问题。
一个和二分匹配有关的问题叫稳定婚姻系统。似乎在OI中用途不太大(主要是不太具扩展性),问题和算法(延迟认可算法)我不描述了。
(第10章暂略)
第11章
图就是由顶点和边构成的东西,(无向或有向的)边可以认为是顶点中的一种(可交换或不可交换的)关系。顶点的度就是邻接的边的数量,如果是有向图的话还可以定义入度和出度。
欧拉路就是一条经过了所有边仅一次的(不一定是简单)路径。存在欧拉路的充要条件是有0个或2个度为奇数的顶点,前者的欧拉路的起点与终点是闭合的。欧拉路可以用圈套圈算法来求。不过书中的方法实现起来还是麻烦(需要写链表),有更好的方法。中国邮递员问题,就是求一条从一个点出发经过每一条边再回到起点的最短路径。解决这问题的大概想法就是通过添边把所有奇度顶点变偶。问题的算法书上没写,不过我想大约就是:找出所有奇度顶点求它们两两的最短路,然后求(非二分图的……)最优匹配。如果是有向的反而简单的多,因为这样可以根据负点和正点构造出二分图匹配。
Hamilton路径的定义就是一条经过所有顶点仅一次的路径。这是NP-Complete问题,目前没有多项式算法。
判断图是否是二分图只需要试图给图2涂色,如果涂出矛盾了就不是。
树是一种图,它的定义包括:任两点都有且仅有一条路径的图;不含圈的图;n个顶点n-1条边的连通图。
Shannon开关游戏很有意思。正方必胜策略的有无取决于图是否存在两个不存在重复边的生成树,如果找出了这两个生成树,必胜策略的算法还是不太复杂的。不过这两个生成树怎么找呢?
求最短距离树,其实就是求单源最短路径,所有的最短路径(可以)是一棵源点为根的树,用Dijkstra算法求。最小权生成数用Kruskal算法或Prim算法求。
第8章
本章介绍了很多著名的数列,我认为这是本书最激动人心的一章。
Catalan数:C_n = C(2n,n) / (n+1);C_n = ΣC_i*C_(n-i),其中0≤i<n;C_n=C_(n-1)*(4n-2)/(n+1)。它的意义有很多,例如:n+1边形用对角线划分成三角形的方法数;n个+1和n个-1满足所有部分和不小于零的排列数;具有n个节点的二叉树的数量……
对于数列h_i,定义它的(一阶)差分序列Δh_i=h_(n+1)-h_n。它的p阶差分序列就是p-1阶差分序列的差分序列。如果某个序列是n的p次多项式,它的p+1次差分就全是0,p次差分是常数列。利用这个原理可以在给出前n+1项的前提下求解这种序列。差分运算满足对加法的分配率。
序列差分表由它的第0行确定,也就是原序列,但同时也可以由第0条对角线上的元素确定。换句话说,由差分表的第0条对角线就可以确定原序列。有这样两个公式:原序列为h_i,第0条对角线为c_o,c_1,…,c_p,0,0,0,…则h_n = c_0*C(n,0)+c_1*C(n,1)+…+C(n,p),Σh_k(k=0..n) = c_0*C(n+1,1)+c_2*(n+1,2)+…+c_p*C(n+1,p+1)。记住这两个公式,差分表(的第0条对角线)就变得非常有用了。
第二类Stirling数:S(p,k) = k*S(p-1,k) + S(p-1,k-1);S(p,0)=0(p≥1),S(p,p)=1。它的意义是将p个元素的集合划分成k个不可辨别的非空集合的方法数。上面的递推式可以用组合证明:一方面,如果将元素1单独拿出来划分成1个集合,那么方法数是S(p-1,k-1);另一方面,如果元素1所在的集合不止一个元素,那么可以先将剩下的p-1个元素划分好了以后再选一个集合把1放进去,方法数是k*S(p-1,k);有加法原理得证。第二类Stirling数有公式:S(p,k) = (Σ(-1)^t * C(k,t) * (k-t)^p) / k!,不过看上去好像意义不大,记住一开始的递推式就好了。
Bell数B_p表示将p个元素的集合划分成一些不可辨别的非空集合的方法数,由第二类Stirling数的定义,显然有:B_p = S(p,0) + S(p,1) + …… + S(p,p)。还有一个递推式:B_p = ΣC(p-1,i)B_i(0<=i<p),可以组合证明。
第一类Stirling数s(p,k)满足的递推关系是:s(p,k) =(p-1)*s(p-1,k) + s(p-1,k-1);初始条件与S(p,k)一样是s(p,0)=0(p≥1),s(p,p)=1。它表示n个物体排成k个非空的循环排列(就是圆圈)的方法数。
第一类和第二类Stirling数事实上的意义比上面讲到的要丰富,它们是P(n,p)与n^p这两组多项式向量空间的基互相表示的系数。哦……这个不理解没关系的。
将整数n写成一个无序的正整数的和式的方法数就是分拆数p_n。p_n相当于方程n*x_n + … + 2*x_2 + 1*x_1 = n的整数解的个数。书中没有给出递推的或者直接计算的式子,只给出了一个生成函数,实际计算的时候加一维用DP求解就好了。
用n个一般位置上的(k-1)维超平面分割k-维空间所成的区域数是h(n,k)=C(n,0)+C(n,1)+…+C(n,k)。一般的推导较繁。
格路径,就是在平面直角坐标系的整点中从一个点到一个点的最短路径。从(0,0)到(p,q)的格路径数是(p+q,p),因为一共要走p+q步,选择其中p步向上走。下对角线格路径就是从源点到目标点的对角线的上方(不包括线上的点)不可到达时的格路径数。从(0,0)到(n,n)的格路径数就是Catalan数C_n,根据那个+1、-1部分和很容易证明。一般地,从(0,0)到(p,q)的下对角线格路径条数是R(p,q)=C(p+q,q)*(p-q+1)/(p+1)。如果允许对角步进,计算会有点麻烦,给一个公式好了:从(0,0)到(p,q)经过r个对角步进的格路径数K(p,q:rD)=C(p+q-r,(p-r,q-r,r))(这是多项式系数);如果下对角线,更加麻烦,R(p,q)=K(p,q)*(q-p+1)/(q-r+1)。
大Schroder数R_n就是上面讨论过的R(n,n),所以R_n=。小Schroder数s_n指给n个数“加括号”的方法,“加括号”的规则是:一个数不能加括号,可以给任意多个数的序列加上一个括号并在之后把它当作一个数处理,整个序列最外面的括号可以省略。有结论R_n=2*s_(n+1),用生成函数的方法证明。
上面这些东西重要的是掌握思想,特别是组合证明,至于式子的记忆还是次要的。
第10章
变量,显然是程序里最多使用的东西。我认为这是在OI中改善程序清晰度最重要的一个环节。
声明应该尽量靠近变量第一次使用的位置,初始化靠近声明。尽量减少变量的作用域(存活时间),能局部就不要全局,循环变量不在循环体外部使用的话就声明在内部。能采用const的不要采用神秘数值,如果保证只出现一次的话可以例外。不要采用节省一两个字节的奇怪技巧,比如说同一个变量会先后代表两个不同的事物。确保所有声明的变量都有被使用。
并不像伪代码编程过程之类的方法学,这些软件工程技巧都是“零代价”的。所以为何不将它们引入到OI中呢?
第11章
在OI中,关于变量的命名,唯一的目的是保证你自己能够在整个过程中都能完全清晰的理解。可能的情况下让它尽量自描述可以帮助理解。更值得称道的是你自己养成的从不会搞错的根深蒂固的命名规则。“何时采用命名规则”的清单里,没有OI能对上号的。变量名字没有意义(i,j,k)不要紧,值得担心的是变量名字的字面意义和实际用途不一致。
第12章
避免使用神秘数值,说过好多遍了。事实上,一般使用const声明的原因是因为你可以方便地改动它。
整数需要关注的问题是除法和溢出。除法的规则已经了解,溢出的问题需要加强估算。(我还是记不住2 147 483 647这个神秘数……还是(~0)>>1好了。)
浮点数的加减运算也会出问题,在数量级相差巨大的数之间。等量判断的常识应该是众所周知的。
数组中,需要关心的是下标的溢出,以及嵌套循环中的“下标串话(cross-talk)”。
第13章
结构体可以明确数据的关系。例如几个看上去无关的数组其实是在表示一组元素的多个不同属性。这时若定义一个struct,并使用这个struct的数组可以使代码清晰一些,但也会加大代码长度。值得权衡。当它们可能被作为一个整体来使用时,例如交换甚至排序,那么毫无疑问应该用struct了。
指针是令人亦爱亦恨的东西。它的确很方便,能简化一些东西,但也是很多很多错误的源泉。至于指针的理解,呵呵,每一个真正的C程序员都理解的。
把指针操作限制在子程序或类里面我是同意的。不过若一个名为NextLink()的函数的唯一一行就是i=i->next的话,也太形式主义了点。
应该把指针看作更“易碎”的变量来看待。对待变量的原则——声明与初始化与首次使用尽量接近之类——应该更加严格地施加于指针之上。指针的运用还是应该尽量减少,但决不应该“惧怕”到自己用数组模拟一种指针出来,那会能使强类型变弱。当指针仅仅是为了使接受它为参数的子程序能够修改此变量的时候应该使用引用。
全局变量在OI中的地位有点微妙。由于一个OI程序本身研究的是一个很“局部”的问题。所以所有和整个问题相关的变量(比如说输入进来的数据)都可以全局。真正需要避免的是把确实“局部”的东西,例如循环中的i、j、k都弄成全局的。
第7章
(作者太厉害了,竟然举了一个例子就介绍了在书写rountine过程中可能犯的几乎全部错误。)
创建子程序的最初始目的还是为了避免重复,但是在现代编程中,这不是唯一的目的。降低复杂度,更高层次的抽象,隐藏某些信息是目的中最主要的,其它目的也都很有启发性,因为它们可以最明确无误地告诉我们何时使用子程序。
即便一个看上去过于简单没必要写成子程序的操作,只要它确实多次重复,出于更好的(是的,没有最好的)可读性的目的,还是提倡把它写成子程序。另外,短小但重复的代码带来的另一个问题是无法变化。
在子程序的层面,内聚性的意思就是一个子程序里面做的事情应该是彼此紧密相关的。功能上的内聚性是首要的,在OI中我们应该做到一个子程序只做一件事。
子程序的名字很重要,虽然在OI中似乎不用拘泥什么规则,但最好还是形成自己固定且清晰的命名习惯。当你发现你不能用简短清晰的名字说明子程序所作的事情时,这个子程序的设计大概有问题,比如说有副作用。
也许子程序的长度是一个有意思的研究领域,但是在OI中大约还没有真正“长”的子程序。我近来不喜欢在OI中为子程序而子程序的做法,也就是说任何程序都有Input、Solve、Output这样的子程序(我以前这么做)。我现在认为在OI中不重复的代码完全没必要做子程序。
对于参数表的参数顺序,首要原则我认为是重要的变量放在前面,同时参数列表相似的子程序其顺序应该一致。
函数(在C-like的语言中,特指非void函数)的返回值在某些执行路径中可能会忘记返回值,g++对此也没警告。这是我经常犯的错误……很多时候都会为此调试半天……一定要注意这个。
对于子程序的性能,不要臆测,要知道只有Profile能告诉你真正的瓶颈所在。
第8章
在OI中,一般是完全不需要“防御式编程”的。我常常采用的方法是给需要传入的数据满足一定条件的函数处加上注释,而不是在程序中采取断言之类的措施。
第9章
在写程序前写高质量的伪代码的确是个提高代码质量的好主意,但在竞赛中还是把这个过程留在脑海中吧。不过以后有空的话可以考虑把所有常用的算法自己写一份伪代码,写完以后与CLRS之类的书上的伪代码进行比较。(其实也说不定自己写的才更好哦。)