ACM总结报告

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/nothingchoice/article/details/51783759

告别了刷不完题的高中时代,我们来到了大学,各种各样的社团或者是比赛如约而至,而acm比赛却是给我最印象最深的一个。

  我与acm结缘是在大一的学期末,负责我们c++教学的朱红梅老师让我们选一下ACM课,由于各种原因,当时并没有选上,想来想去又想多学点东西,就偷偷跟着其他的同学去听ACM课了,当时的C++也只是学了点皮毛,并不理解什么是泛型,什么是迭代器,去上课的时候也只是听听解题的思路,看不懂具体的代码,直到后来自己看完了C++ primer plus那本C++入门的书和相同版本的C语言入门的书——c primer plus,这些也都是后来的事了。也是因为那时候对C++并没有什么理解,再加上学习ACM很费时间,所以后来就和ACM脱节了,至于假期里买的机械工业出版社出版的那本算法导论,到现在也没看。

  虽然大一的时候听了几节ACM课并没有对我的知识增加多少,但是这让我看清楚了自己与其他人的差别,后来也就有了看那两本1000页的c primer plus 和 C++ primer plus的事了,也坚持看完了,虽然那两本书写的很浅,但对于当时的我来说已经很够用的了,值得一说的是,当时并不知道惠普专业到底和计算机有什么不同,所以大一寒假的时候和一个泰山医学院的同学一起去了趟济宁惠普基地,在那里两个校管部的老师招待了我们,上午我们参观了济宁的惠普基地,下午他们专门为我们请了一个济宁惠普学校的老师,给我们讲了讲问题,当我问到关于ACM的问题时,他告诉我,程序就是算法加数据结构,ACM程序设计课可以很大的提高我们的编程水平,后来当我问到什么编程语言好的时候,他告诉我国内主要用java,国外主要用C语言,也就有了后来的我自学C语言。

  大一下学期的时候我基本上在学C语言,到了大二和ACM接触的也就更少了,基本上也就是java那一套了,直到后来课程表下来,ACM程序设计赫然在列。后来发生了一件很奇怪的事,ACM居然是专业限选课,居然可以删除,身边很多小伙伴都删了。当时我的决心丝毫没有动摇,那就是学ACM,最后14级计算机留了ACM课的也没有几个人了,时隔一年,当我再次步入ACM的讲堂的时候,我什么都懂了,什么是泛型,迭代器,队列。还是和一年前一样的教学流程,那么熟悉,但和以前完全是两个不同的体验了,上了16周的ACM课,感觉学到了很多知识,虽然现在已经是靠java程序设计吃饭了,但是我还是想说,ACM真的很有用,不管你以后去向何方。

  下面我就把这16个周的学习总结报告写在下面。

  再这个寒假的时候,老师就让我们在家自学ACM中的常用类,例如list,map,迭代器。假期里老师也给我们开放了几个专题让我们自己做,刚开始的时候做了几道,后来因为太懒散,也是在家里的原因,也或许是因为自己学过的知识都会了,就没有继续往下做。但是这次刷题再也没有给我大一的时候ACM的恐惧感,完全是轻车熟路,不费多大事。

  开学后的第一个周的周日,ACM终于在5n开课了,看了看来的人,14级的还是比较偏少。第一个专题是贪心算法,所谓贪心算法就是在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。贪心算法可解决的问题通常大部分都有如下的特性:选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解,为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪心算法正确工作,那么找到的第一个解通常是最优的。贪心算法最大的问题在于如何贪心,贪心的标准是什么样的,这才是我们需要解决的问题。往往贪心算法找到的最优解,并不是真正意义上的最优解,而是由局部最优解拼凑出来的“最优解”。在第一个专题中自己刷题也是属于比较少的那种,至于原因有很多,也或许是因为其他的项目没把学习的重点放在acm上面吧。

第一个专题很快就过去了,我们又步入了第二个专题--搜索专题,就是去年听了让我头都大的一个专题,这里面用到了各种“奇葩”的东西,比如当年的软肋--递归函数。搜索专题,平心而论,还是没有想象中的那么难以理解。常见的搜索算法分为了深度优先搜索(DFS),广度优先搜索(BFS),二分搜索,三分搜索。

所谓的BFS的思想就是从一个图的某一个顶点V0出发,首先访问和V0相邻的且未被访问过的顶点V1、V2、……Vn,然后依次访问与V1、V2……Vn相邻且未被访问的顶点。如此继续,找到所要找的顶点或者遍历完整个图。

DFS的思想:深度优先搜索所遵循的策略就是尽可能“深”的在图中进行搜索,对于图中某一个顶点V,如果它还有相邻的顶点(在有向图中就是还有以V为起点的边)且未被访问,则访问此顶点。如果找不到,则返回到上一个顶点。这一过程一直进行直到所有的顶点都被访问为止。 DFS可以搜索出从某一个顶点到另外的一个顶点的所有路径。 由于要进行返回的操作,我们采用的是递归的方法。

至于二分查找算法,针对的是单调函数给定函数值,求自变量值的情况,非常简单,优点是比较次数少,查找速度快,平均性能好,首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

三分算法:把区间分成三段,或者函数求导再进行二分,就形成了所谓的三分。

搜索算法的题也基本上是千篇一律,只要掌握了这四种算法,就很容易做出来,但是一定要注意,搜索算法是一个空间复杂度和时间复杂度都很高的算法,a题的时候一定要注意时间和结合具体的题进行优化,否者很容易超时的。

后来就到了第三个专题,在这个专题里面,acm成功抱上了运筹学的大腿,运筹学的一个分支--动态规划,在经管学院报了个双专业—会计学,里面有一门课叫管理运筹学,但是双专业所讲的动态规划比较简单,而且也没让用代码实现,也算是从零开始吧。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

常见的动态规划有:

类斐波那契数列问题,指特征方程类似于 f(n) = f(n-1) + f(n-2),的问题,此类问题最直观的题就是斐波那契数列,稍微难一点的就是跳楼问题,和堆砖头问题,这两个题虽然看起来与斐波那契没什么关系,但是经过分析和转化之后,就可以转化为该类型。总的来说,类斐波那契数列问题是这套专题里最简单的问题,只要做会了几个典型,其他的就很简单了。

 01背包,一般描述为给定背包的容积,一些物品的价值和体积,求能放入背包中的物品的最大价值,一个比较通用的公式为f[i, j] = max( f[i-1, j-Wi] + Pi (j >=Wi), f[i-1, j] ),这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

 完全背包,这个问题非常类似于01背包问题,所不同的是每种物品有无限件,也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……取[V/c]件等很多种。

最大公共子序列,又称LCS,由于上课老师没有讲过,便自己学习了一下,这类问题一般描述为给定一定个字符串,求出个个串最长的公共子序列的长度,这种问题的解决思路很巧妙,用一个矩阵,行和列每个格子代表一个字符,分别表示出两个字符串,如图,引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定搜索的方向。我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] = Y[j]还是X[i] != Y[j],就可以计算出c[i][j],即问题的解。

这一章的题主要是注意如何运用这几个标准的模板来进行刷题,要注意超时问题,这章的题开放得时间也很长,这一章我大约做了大约22道,基本上每一道题都是自己完成的,所以,感觉收获也蛮大的,至少运筹学考试的时候动态规划部分,不用复习了。

刷完第三个专题后,我们来到了acm的终结部分,图论的部分,在结束了对运筹学的“友好访问”后,acm又坐上了其他学科的车,图论在离散数学中学过其中最基本的理论知识,prim等算法也都学过,又在数据结构中学过图论最基本的代码表示方法,以及图的一些基本算法,学习这一章当然不是什么难事。

关于图论的学习,它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。

还有一些图论中常用的概念

子图:当图G'=(V',E')其中V‘包含于V,E’包含于E,则G'称作图G=(V,E)的子图。每个图都是本身的子图。

生成子图:指满足条件V(G') = V(G)的G的子图G。

度:一个顶点的度是指与该顶点相关联的边的条数,顶点v的度记作d(v)。

入度和出度:对于有向图来说,一个顶点的度可细分为入度和出度。一个顶点的入度是指与其关联的各边之中,以其为终点的边数;出度则是相对的概念,指以该顶点为起点的边数。

路径:从u到v的一条路径是指一个序列v0,e1,v1,e2,v2,...ek,vk,其中ei的顶点为vi及vi - 1,k称作路径的长度。如果它的起止顶点相同,该路径是“闭”的,反之,则称为“开”的。一条路径称为一简单路径(simple path),如果路径中除起始与终止顶点可以重合外,所有顶点两两不等。

行迹:如果路径P(u,v)中的边各不相同,则该路径称为u到v的一条行迹。

轨道:如果路径P(u,v)中的顶点各不相同,则该路径称为u到v的一条轨道。

闭的行迹称作回路,闭的轨称作圈。

基本概念只是基本概念,如何在代码中表示出来确是另一回事了

一个不带权图中若两点不相邻,邻接矩阵相应位置为0,对带权图(网),相应位置为∞。一个图的邻接矩阵表示是唯一的,但其邻接表表示不唯一。在邻接表中,对图中每个顶点建立一个单链表(并按建立的次序编号),第i个单链表中的结点表示依附于顶点vi的边(对于有向图是以顶点vi为尾的弧)。每个结点由两个域组成:邻接点域,用以指示与vi邻接的点在图中的位置,链域用以指向依附于顶点vi的下一条边所对应的结点。如果用邻接表存放网(带权图)的信息,则还需要在结点中增加一个存放权值的域。每个顶点的单链表中结点的个数即为该顶点的出度(与该顶点连接的边的总数)。无论是存储图或网,都需要在每个单链表前设一表头结点,这些表头结点的第一个域data用于存放结点vi的编号i,第二个域firstarc用于指向链表中第一个结点。

在上面两个图结构中,一个是有向图,即每条边都有方向,另一个是无向图,即每条边都没有方向。

在有向图中,通常将边称作弧,含箭头的一端称为弧头,另一端称为弧尾,记作<vi,vj>,它表示从顶点vi到顶点vj有一条边。

若有向图中有n个顶点,则最多有n(n-1)条弧,我们又将具有n(n-1)条弧的有向图称作有向完全图。以顶点v为弧尾的弧的数目称作顶点v的出度,以顶点v为弧头的弧的数目称作顶点v的入度。在无向图中,边记作(vi,vj),它蕴涵着存在< vi,vj>和<vj,vi>两条弧。若无向图中有n个顶点,则最多有n(n-1)/2条弧,我们又将具有n(n-1)/2条弧的无向图称作无向完全图。与顶点v相关的边的条数称作顶点v的度。

最小生成树

在一个图中,每条边或弧可以拥有一个与之相关的数值,我们将它称为权。这些权可以具有一定的含义,比如,表示一个顶点到达另一个顶点的距离、所花费的时间、线路的造价等等。这种带权的图通常被称作网。

图或网的生成树不是唯一的,从不同的顶点出发可以生成不同的生成树,但n个结点的生成树一定有n-1条边

构造最小生成树的方法:最初生成树为空,即没有一个结点和一条边,首先选择一个顶点作为生成树的根,然后每次从不在生成树中的边中选择一条权值尽可能小的边,为了保证加入到生成树中的边不会造成回路,与该边邻接的两个顶点必须一个已经在生成树中,一个则不在生成树中,若网中有n个顶点(这里考虑的网是一个连通无向图),则按这种条件选择n-1边就可以得到这个网的最小生成树了。详细的过程可以描述为:设置2个集合,U集合中的元素是在生成树中的结点,V-U集合中的元素是不在生成树中的顶点。首先选择一个作为生成树根结点的顶点,并将它放入U集合,然后在那些一端顶点在U集合中,而另一端顶点在V-U集合中的边中找一条权最小的边,并把这条边和那个不在U集合中的顶点加入到生成树中,即输出这条边,然后将其顶点添加到U集合中,重复这个操作n-1次。

无向图的邻接矩阵

具有n个顶点的无向图也可以用一个n′n的方形矩阵表示。假设该矩阵的名称为M,则当(vi,vj)是该无向图中的一条边时,M[i,j]=M[j,i]=1;否则,M[i,j]=M[j,j]=0。第i个顶点的度为矩阵中第i 行中"1"的个数或第i列中"1"的个数。图中边的数目等于矩阵中"1"的个数的一半,这是因为每条边在矩阵中描述了两次。

学习了基本的概念,还有一些基本的算法。

例如,prim算法:

1).输入:一个加权连通图,其中顶点集合为V,边集合为E

2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;

3).重复下列操作,直到Vnew = V

a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且vV(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

b.v加入集合Vnew中,将<u, v>边加入集合Enew中;

4).输出:使用集合VnewEnew来描述所得到的最小生成树

《《数据结构》》这门课中讲的比较细,例如还有什么时间复杂度,空间复杂度,这里也不一一赘述了。

还有kruskal算法,即求加权连通图的最小生成树算法kruskal算法总共选择n- 1条边,(共n个点)所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中。注意到所选取的边若产生环路则不可能形成一棵生成树。kruskal算法分e步,其中e是网络中边的数目。按耗费递增的顺序来考虑这e条边,每次考虑一条边。当考虑某条边时,若将其加入到已选边的集合中会出现环路,则将其抛弃,否则,将它选入。

图论的东西真心不多,好好学还是很简单的。

现在我已经完全是一个web程序员了,写程序基本上也就是html+css+js这一套了,后台基本上也就是用java的那些javabean,servlet那些东西,是不是觉得acm程序设计已经用不到了呢?那你就大错特错了,虽然那些算法在java中已经被类给封装好了,但是要更深层次的理解,复用,还是需要acm,数据结构的基础知识,老师在开课的时候也说了,acm程序设计可以更好得锻炼我们的大脑,脑子用活了,考研的时候也不用再担心什么了。

总之,学习acm益处多多。

猜你喜欢

转载自blog.csdn.net/nothingchoice/article/details/51783759