当前位置 博文首页 > budatu的专栏:杜宇学长的总结——深受感动

    budatu的专栏:杜宇学长的总结——深受感动

    作者:[db:作者] 时间:2021-08-18 13:20

    做算法和作技术哪个难?都很难,没有可比性。但是算法做得好的可以转行做技术,技术做得好的想转行做算法却很困难。?

    我是08年下半年将近期末的时候加入华理ACM队的。我高中的时候没有编程经验,数学也不好,高考数学刚及格。因为第二工业大学的网络工程专业的分数是最低的,所以就比较巧合地步入了计算机行业。大一有一门C++课程,当时我在第一次上机的时候就深深的被C++迷住了。C++是一门极其优美的语言,相比于高中计算机课上一笔带过的VB,我最喜欢C++的花括号。因为学校比较差,周围的学生没几个专心读书的,所以对于一个稍微想要学点东西的学生来说,这样的学校里的硬件资源就显得异常丰富了,图书馆里很多计算机经典书都是新的,而且自修室人也很少,整个大一除了谈谈恋爱解解闷以及陪室友打打魔兽以外,我大多数时间都在图书馆或者自修室,虽然在那里的时候不总是做些学习方面的事情……我也会经常无聊的在网上乱逛或者到处下载资料做收藏家。但正是这个时候我接触到了C++的圣经《The?C++?Programming?Language》,当时我看了一段时间的电子版,因为不方便作笔记,很不爽,于是在081月的时候买了中译本。之后的大一下学期我又陆陆续续看了部分《Effective?C++》,《More?Effective?C++》,还有一些数据结构方面的东西。然后学期中期的时候我集中精力做了一段时间的数学建模竞赛。大一结束的时候我通过插班生考试从第二工业大学转来华理,很不巧,插班生考试数学又是刚及格。在暑假期间我去上了CCNACCNP的部分课程,因为感觉这种纯操作工一样的"背诵"活很不适合我,于是半途而废了,暑假在家的时候做了很多windows下的网络编程,自己写发包程序,写SYN攻击很有意思。?

    大二刚来到华理的时候自我感觉很牛B,因为我自学过的东西几乎都没有人能来跟我做做讨论。于是我自以为很了不起,也没怎么在意学校的课程,除了上课,平时我几乎都在图书馆或者自修室。这期间我接触了设计模式,以及.NET开发。大约在开学不久,08年的Regional还没有开始的时候,我听说了ACM这种东西。当时对这个东西我是完全没概念的,在某个晚上我去奉贤311找了罗老师,当时咏天也在,是ACM队准备召新的时候,罗老师跟每个同学谈了话,我是最后一个,跟他哗啦啦地说了很多以前我看过的书,做过的东西。被罗老师赞赏了一番之后我更加自以为是了,并以非正式的身份进了ACM队。?

    但是直到学期结束,我也没做什么题。学期将尽的时候我跟室友展开了一个旧书交易网站的建设,然后我就一股脑地完全投入进去了,用了将近五个月的时间,其中占用了我整个寒假,我用ASP.NET+Spring.NET框架写了一个自以为有很高技术含量的简单的旧书信息发布平台,全站单页面,无刷新,所有操作都用AJAX技术完成,并且我自己设计了一个貌似很牛B的内存数据库。结果在09年的上海市计算机应用能力大赛中,我的作品只获了一个优胜奖。貌似还有很多同学沉迷于这些,以为这就是编程。备受打击的我在迷茫了一阵子之后想起来ACM,于是在大概3月底的时候开始在PKU切题。?


    以上都是之前的一些学习经历,关于ACM的部分从这里开始。?

    ACM是个很有意思的东西。起初,我有点看不起这个东西,相信很多过早接触技术开发方面的同学都有这样类似的想法,于是就有了诸如"做算法和做技术哪个难"的问题。今天再让我来回答这个问题的话,我只能说,都很难,并且没有可比性。但是我要补充一点,大学生可以做技术,不上大学同样可以做技术。再要我说得明白一点的话就是,一年前我用了将近一个星期的时间,看着MSDNCSDN写了一个windows下的扫雷程序,并且在之前我用了若干个星期积累了SDK编程的一些知识,而今天虽然我已经忘记了几乎所有SDK编程的细节,不知道怎么去画一个自定义的按钮,但是给我一台能上网的电脑和一天的时间,我能把当时的那个扫雷程序写出来,并且让它的代码量精简掉一半以上。?

    另外一个事实就是,绝大多数的ACMer以及几乎所有成功的ACMer的数学都非常好,不论他们是原来就很好还是后来变得很好。?

    以下说一些实质性的东西。?

    ?编码能力?

    ACM的世界里,编码能力和数学功底是绝对的王道,硬要分个先后的话,编码能力就是王道中的王道。ben在学校的时候几乎不看算法类书籍,最多也就是看看网上零散的知识点,但是他的成绩是大家有目共睹的,其中最重要的一点就是他的编码能力至少在华理,那是令人发指的强。写一个容斥原理你要多久?或者给你入射光和球的相关参数,要你编码求出反射光的运动参数,你又要写多久?更有甚者,在今年暑期的个人赛第一场最后一题,给你一个扫雷过程中的状态,要你计算出所有能够确定的雷,这题ben用了不到半个小时就AC了。?

    今年的上海赛区同样对编码能力要求很高,其中我们没有过的I题,当时ben打印出来的代码有4张纸,也正是因为这么长的代码,我跟sky连去纠错的信心都没有,结果ben自己也没有能够在赛场上把错误找出来。?

    编码能力的培养的捷径就是做题,尤其是搜索,模拟,计算几何等类型的题目尤其锻炼编码能力。我能够很快适应ACM,很大程度上也是得益于大一期间积累的编码能力和C++语言基础。?

    在?锻炼编码能力时,特别要注意做题要限制时间,这点跟自己做一些小项目,小程序完全不同,不能拖拖拉拉一做就是几天。最好的方法是在写代码的同时把自己做这题的开始和结束时间都标注上去,这点在后面关于解题报告的部分我还会说到。如果觉得有时自己会记不住记录开始时间,那就养成一个习惯,每看到一题就先随便submit几个字母,让它CE一次,这样以后再回过头来看历史记录就可以知道自己做这题用了多少时间了。限制做题时间又一个很明显的作用就是可以自我认识,即了解自己对于某一方面的知识掌握程度,如果平时遇到某一类型的一般题目(没有特别恶心的trick,没有特别恶心的输入),自己需要花超过2个小时去AC,那么就几乎可以肯定你在赛场上是不太可能把这题做出来的,换句话说,如果你在赛场上遇到这种类型的题目就可以暂时先放一放了。?

    编码能力的一个很关键的地方就是编码风格问题,一些普遍适应的原则诸如"不要把复杂的语句写在[]""不要在if里写复杂的语句""if里复杂的逻辑拆分开""全局变量要特殊命名"等等,这里不可能一一列举,唯一的办法就是每次自己在这里吃到苦头的时候把它记录下来。具体的可以参考ben的方法,例如把PKU的每次提交情况都粘贴进一个文本文件里,然后在每次提交记录后面都加上一些注释,比如"++i写成i++",或者"dis数组忘记初始化"之类的东西,这样即使不能保证此类错误以后不会再犯,也至少可以降低你犯此类错误的概率。关于降低犯错的概率还有一个生理学上的解释,即记忆的编码并不是纯粹的把东西原封不动地放进脑子里,而是会被我们的神经系统先分解,储存在大脑各处,并且其中夹杂了大量记忆的场景,即记忆发生时的环境。当我们再次处于类似的环境时就有更大的概率把相关的事情回忆起来,所谓的联想记忆也是这么一回事。所以当我们犯错误时就应该尽可能地增加可以勾起我们回忆的材料,比如写下一些箴言形式的语句加深记忆。?

    ?解题报告?

    上面说到错误记录可以降低我们犯错的概率,同样的,解题报告也是基于这一原理,并且它往往比错误记录更加有效。?

    我们常常可以看见一些大牛blog上的学术类文章写得非常风趣,有时里面会参杂一些很搞笑的例子,于是他们的文章较之纯理论的文章更容易被我们记住。这仍然是基于上述的生理学原理,我们用来会议的材料越丰富,或者这些材料越投我们所好,我们就越容易联想起来与这些材料相关的东西。这个原理应该用到些解题报告上来,那就是尽量把自己关于这题的,一些有意思的想法记录下来。另外,记录自己在解决这题时的思维过程也是非常有益的。如果你还不知道思维过程的重要性,那么请去看看波利亚的《How?To?Solve?It》(中文译名"怎样解题"),这是每一个理工科学生的必读读物之一,甚至你可能会在看完此书的若干年以后才会突然体会到其中的一些条款的极端正确性,所以,越早看这本书越好。?

    关于思维过程,并不是十分容易被记录下来,并且,在你能够明明白白把你对于某题的思维过程叙述出来之前,你对于这题的解决始终都是不完整的,即下次遇到此类题目的变种,十有八九还是无从下手(某些大牛可能因为语文水平不足而无法很好用语言表达,这是特例……)。?

    写解题报告的另一个好处当然是有助于自己复习。尤其是在比赛之前看看解题报告是非常有益的,一般情况下,就拿我来说,在两个月内做过的题目,只要扫一眼解题报告,一般我能马上把它原样地用代码实现出来。?

    如果不是很确定自己能坚持写解题报告,或者担心自己的解法的正确性(很多时候AC了的代码不一定是正确的),那么就去写blog吧,把解题报告贴到blog上吧,也许会有人用很尖锐的言语指出你的错误,但,那不正是你想要的吗?另外贴到blog上也方便了自己日后的搜索。?

    ?关于图论?

    论资格,我绝对在ACM队是派不上号的,切题数也十分寒酸,于是当你处于这种情况的时候就应该考虑主攻某一方面,毕竟ACM是一项团队比赛,一把瑞士军刀总也没有几把专业的刀具功能强大。我在队里主要负责的是图论,所以我在这里着重说一下图论方面的东西。?

    ACM题目类型主要分为DP,图论,搜索,数据结构,模拟,计算几何,字符串,组合数学,数论等,其中前两个是重头戏,我做过的每场比赛里,前两种题目都是必然会出现的,DP很大程度上需要依靠YY,即它与IQ的关系很大,这几乎是一个毋庸置疑的事实……并且DP的某些思想贯穿大部分ACM题目,很容易于其它类型题目融合起来,想要掌握它非一朝一夕之事。而图论相对来说并没有DP那么可怕,比较容易入门,并且很多图论类题目可以套模板,但是相对的,图论题目也可以出得令人发指的难,并且其数学模型往往隐藏在搜索,计算几何,字符串等类型的题目后面,即表面上看起来不是图论,但实际上这题考的却是图论原理。?

    图论的变形繁多,ACM题目中尤以Dijkstra最多,看似简单的Dijkstra,其变形程度是相当可怕的,比如会消耗汽油的车的最短路问题,这就是一个相对简单的二维Dijkstra,而更加复杂的,例如08年哈尔滨赛区的H,一道隐藏在搜索背后的三维Dijkstra,全场没有队伍出。解决这类问题的一个根本性方法就是充分理解Dijkstra的定标技术,以及规范的状态表示。何为规范?即当状态维数增高时,需要对应的结构定义其状态,并且此结构体切不可与存入堆的结构相混淆,只要明确了Dijkstra的状态表示以及在某些限制条件下的状态转移(即图中的""),则高维Dijkstra就不再是无法攻克的拦路虎了。?

    图论的另一个大头就是网络流,这是图论中最容易套模板的一部分,也是极其困难的一部分。说容易套模板是因为往往这类题目在赛场上本身就是中等题或难题,如果再不能套模板,八成就变成了无人能出的大自然题了……网络流的变形数量几乎可以赶的上Dijkstra了,如果算上匹配类的题目则就是有过之而无不及。网络流基本可以分为最大流,限制流,费用流三种,其中最大流可以变形为二分匹配,费用流可以变形为带权匹配。其中最大流的算法是其它几种流算法的基础,主流算法可以分类增广路类和预流推进类,这两类算法几乎没有联系,对于ACM,只要学习前一种就足够了。?

    网络流类题目,包括匹配类题目的核心思想就是增广路,在匹配类题目中特化为交错轨,这也是增广路类算法的核心。各种增广路类算法的区别大都在于如何快速找到一条增广路,其中比较简单的EK就是每次BFS?

    找到一条增广路,我就不多说了,而高级一点的ISAPDinic都属于SAP,两者都是基于分层图的思想,实现分层图的方法只要为每个顶点标记所在层号即可,其中Dinic是通过一次BFS建立分层图,然后按照建立好的分层图进行多次DFS找到多条增广路,从而不用像EK那样每条增广路都做一次BFS。而ISAP则是在DFS的同时建立分层图,即遇到DFS前进不了的时候对下一顶点重新标号,于是这张分层图是逐步建立的,建立后可以被后面的DFS所利用,从而降低寻找增广路的消耗。DinicISAP都可以用"当前弧"技术进行优化,而ISAP还有一个进需要添加一行代码的GAP优化,具体实现很容易在网上搜到。这里要特别说一下DinicISAP的适用范围,ISAP是万金油,几乎可以应付绝大部分最大流和限制流的题目,而Dinic特别适用于有向无环图,即画在纸上可以分层的题目,此时Dinic往往只总共需要做一次BFS(因为没有可以回退的边),这时Dinic往往会比ISAP快很多。要知道网络流是一个极度悲观的世界,任何已知的算法都没有能突破On^3),所以最大流算法写得不好很容易超时。至于费用流,可以基于EK算法,只要将BFS改为SPFA就可以应付几乎所有的费用流问题了,少量需要用到消负环算法的题目只要用SPFA找负环即可。而限制流则只是一个定式的建图,没什么特别的地方。最后还有一种限制费用流,如果遇到,几乎肯定就是大自然题,可以直接无视之。关于而分图匹配的算法,网上资料很多,核心思想还是基于最大流,只是可能不用最大流来解释也是可以的。其中用于带权匹配的KM算法只要准备好模板即可,一般不会有太大变形。?

    上面一段说的是既有的算法,大部分都可以模板化,其中要注意准备模板的时候最要准备针对整数和浮点数的两种,对于C++程序员来说,相应的只要写成函数模板,然后传入比较函数对象即可,代码量几乎不会有什么增加。?

    下面要说说网络流类题目真正的难点之一,建图。网络流类题目难在它往往伴随着一个不太直观的建图,其中有些利用到最大流最小割定理的建图已经有了套路,比如说限制流的建图,最大权闭合图等,具体可以参见国家IO集训队2007年胡伯涛的论文。另外一些建图则很有技巧性。比如"比赛"类题目,有很多场比赛,要你求得分能否达到某种条件等,这时需要把每场比赛看成顶点,然后两条流进来,只有一条可以出去,这种题目的特色是存在大量容量1的边。还有拆点建图,这类题目往往一个顶点上包含了2"属性",而网络流算法中,属性是体现在连在边上的,尽量要使每个顶点表示的属性单一化,于是就把一个点拆分成两个点,然后把属性分配出去,比如PKU有一题说企鹅在冰块上跳来跳去,冰块就有两个属性,一个是冰上的企鹅数量,另一个是冰块再被起跳多少次后会碎掉,这时就需要把两个属性拆分开来,拆点的另一个应用就是求最小点隔集,其中的思想就是像这题冰上企鹅一样,把出度和入度两个属性分离开。另外一些更加巧妙的建图可能涉及逆向思维等技高技巧性的思维方法,这里就不一一列举了。?

    网络流的难点之二是算法变形,特别是有的题目在增广路上做文章。有一种叫做"连续增广路"的技术,需要深入理解增广路的原理,今年的上海赛区F题就是基于连续增广路的二分匹配加搜索,虽然之前做过一个基于最大流算法的连续增广路,但是很遗憾,当时没有能想到这个算法。另外,与字典序相关的最大流题目往往需要枚举删边,如字典序最小边割集。与其它类型题目相结合的算法变形就多不胜数了,最常见的当属二分答案判可行性,很多貌似运输类的问题很多都可以归结到这种方法上。?

    刚才说道了二分答案,这也是所有图论类(应该说不仅仅是图论)题目最常见的变形,"最(大)小值最大(小)"是这类题目的最一般特征。这类题目常常跟分数规划联系在一起,比如最优比率生成树,最优比率环等。?

    另外图论的一些经典算法可以衍生出很多强大的应用,比如差分约束就是Bellman-FordSPFA的一个最好的应用,这类题目的建图关键是找出差分式子。一个需要特别注意的地方是《算法导论》上对于不连通图上添加顶点的讨论,最好的方法是不用添加顶点,开始时直接将dis数组清0,然后所有顶点入队即可。?

    生成树类的题目也有很多变形,如欧几里得生成树,往往需要利用平面图上的一些几何性质建图,如曼哈顿距离生成树,限制度生成树等等,此类题目套路不多,且知识点比较散乱,需要多做题来熟练。?

    图的连通性也是一个常见的考点。割点,桥,强连通,双连通问题的求解不仅要准备模板,还要充分理解模板。一些地图上的题目(二维的方格阵),往往有些特殊的性质,不仅需要你一些建图的敏感度,偶尔需要在模板中添加一些巧妙的代码。?

    图论还可以与组合数学和计算几何等产生紧密的联系,比如生成树和图的计数,最简单的诸如n个顶点可以产生多少个不同的无向连通图,这时一些组合数学中的基础原理往往十分有用,最典型的就是容斥原理。?

    图论中还有茫茫多的定理,性质等。我所知道的最雷人要数08年哈尔滨赛区那道赤裸裸的Havel定理了。这些定理和性质可能会在一些意想不到的地方发挥作用。?

    最后图论中的一些NP问题或非NP

    下一篇:没有了