转眼间,acm已经结束了,虽然只学习了短短的一学期,但受益匪浅。它不仅教会了我很多知识,而且还让我明白了很多道理。
何为acm,ACM即acm国际大学生程序设计竞赛,是由国际计算机学会主办的,一项旨在展示大学生创新能力、团队精神和在压力下编写程序、分析和解决问题能力的年度竞赛。经过近30多年的发展,acm国际大学生程序设计竞赛已经发展成为最具影响力的大学生计算机竞赛。赛事目前由IBM公司赞助。
这学期总共讲了四个专题,分别是贪心算法专题,搜索专题,动态规划专题和图算法专题。首先,介绍一下贪心算法专题,贪心算法的基本思想是总是做出在当前看来是最优的选择,即找出整体当前中每个小的局部的最优解,并且将所有的这些局部最优解合起来形成整体上的一个最优解,也就是说贪心算法并不是从整体最优考虑的,它所做出的选择只是在某种意义上的局部最优选择,能够使用贪心算法的问题必须满足下面两个性质:
(1)整体的最优解可以通过局部的最优解来求出;
(2)一个整体能够被分出多个局部,并且这些局部都能够求出最优解。
使用贪心算法的两个经典问题是活动安排问题和背包问题。在对问题求解时,不从整体上加以考虑,它所做出的仅仅是在某种意义上的局部最优解(是否是全局最优解,需要证明)。因此,若要用贪心算法求解某问题的整体最优解,必须首先证明贪心算法思想在该问题的应用结果就是最优解。对于一个具体问题,怎么知道能否用贪心算法来解决这个问题呢?一般这类问题有两个比较重要的问题:贪心选择性质和最优子结构性质。所谓贪心选择性质和最优子结构性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一些列局部最优的选择,即贪心选择来达到。做出贪心选择后,原问题就化为规模更小的类似子问题,然后用数学归纳法证明,最终可得到问题的整体最优解。另外,一个重要的性质就是最优子结构,即当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
贪心算法的基本步骤如下所示:
(1)、从问题的某个初始解出发;
(2)、采用循环语句,当可以向求解目标前进一步时,就根据局部最优策略,得到一个部分解,缩小问题的范围或规模;
(3)、将所有部分解综合起来,得到问题的最终解。
虽然贪心算法不能对所有的问题都能得到整体最优解,但是对许多问题它都能产生整体最优解,例如,图的单源最短路径问题,最小生成树问题等。在一些情况下,即使是贪心算法也不能得到整体最优解,其最终结果却是最优解的很好近似,即近似最优解在很多情况下已经能够满足某些问题的求解要求了。
接下来就讲讲搜索专题。在这里用到了关于图的知识,关于图的基本知识,在离散数学、数据结构等相关课程中都曾有过详细的介绍。给定一个图G=(v,e),其中v表示顶点的集合,e表示边的集合。如果图中的边是有方向的,则称该图为有向图,否则为无向图。图的存储方式通常有两种:邻接表和邻接矩阵。图的遍历是从图中某一顶点出发,沿着与顶点相关联的边,访问图中所有的顶点各一次。图的遍历通常有两种基本方式:深度优先搜索和广度优搜索,这两种方法都适用于有向图和无向图。首先先讲一下图的深度优先搜索算法,它类似于树的前序遍历,是搜索算法中的一种。令G=(V,G)是一个有向图或无向图,搜索时沿着树的深度遍历树的结点,尽可能深的搜索树的分支。当结点u的所有边都已被搜索过时,搜索将回溯到发现结点u的那条边的起始结点,这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个过程反复进行直到所有结点都被访问为止。搜索过程如下:
(1)、开始时,图G中的所有顶点都被标记为未访问过;
(2)、从G中任选一点u属于V作为初始出发点,访问出发点u,把它标记为访问过;
(3)、从u出发,搜索u的下一个邻接顶点v;
(4)、若v未被访问过,把它标记为访问过,并把v作为新的出发点u,转到(3),继续递归地进行深度优选搜索;
(5)若v已被访问过,重新从u出发,选择另一个未经搜索过的邻接顶点w,并把w作为新的出发点u,转到(3),继续递归地进行深度优先搜索;
(6)、若u的所有顶点v都已经被访问过,就从u回溯到u之前的顶点。如果u是初始出发顶点,则搜索过程结束。
深度优先搜索是图论中的经典算法,属于盲目搜索,利用深度优先搜索算法可以产生目标图的拓扑排序表,利用拓扑排序表可以方便地解决很多相关的图论问题。广度优先搜索算法也是一种图的搜索算法,是一种盲目搜寻法,目的是系统地展开并检查图中的所有结点,以找寻结果。它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。令G=(V,G)是一个有向图或无向图,广度优先搜索是从根结点开始,沿着树的宽度遍历树的结点。如果所有结点均被访问,则算法终止。搜索过程如下所示:
(1)、从图中选择一个顶点作为初始出发点v;
(2)、首先访问出发点v,然后访问v的所有邻接点w1,w2,……wi;
(3)、接着依此访问出发点与w1, w2,……,wi相邻的,未被访问过的所有顶点;
(4)、以此类推,直到与初始顶点v存在通路的所有顶点都已全部访问完毕为止。
用广度优先搜索算法解决的问题,有如下特点:
(1)、有一组具体的状态,状态是问题可能出现的每一种情况。全体状态所构成的状态空间是有限的,问题规模较小;
(2)、在解答过程中,可以从一个状态按照问题给定的条件,转变成另一个或几个状态;
(3)、可以判断一个状态的合法性,并且有明确的一个或多个目标状态;
(4)、根据给定的初始状态找出目标状态,或根据给定的初始状态和结束状态,找出一条从初始状态到结束状态的路径。广度优先搜索法一般无回溯操作,所以运行速度比深度优先搜索算法要快些。深度优先搜索算法占内存少但速度较慢,广度优先搜索算法占内存多但速度较快。
接下来就讲讲一下动态规划专题。动态规划算法通常具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法十分相似,它的基本思想就是把要求的问题划分成若干个子问题,然后从这些子问题的解中得到原来的问题的解。但是与分治法不同的是,适合用动态规划求解的问题,它的子问题往往不是相互独立的。动态规划又与贪心算法有些类似,往往把一个问题的解决方案视为一系列决策的结果。但与贪心算法不同的是,每采用一次贪心算法准则就要做出一个不可回溯的决策,而在动态规划中,还要观察每个最优决策的序列中是否包含一个 最优子序列。设计动态规划法的步骤如下所示:
(1)、找出最优解的性质,并刻画出其结构特征;
(2)、递归地定义最优值(写出动态规划方程);
(3)、已自底向上的方式计算出最优解;
(4)、根据计算最优值时得到的信息,构造出一个最优解;
在上面的所有步骤中,步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省略,步骤(3)中记录的信息也比较少;若需要求出问题的一个最优解,则必须执行步骤(4),步骤(3)中的记录的信息必须足够多以便构造最优解。
适合动态规划算法的问题本身具有两个非常重要的条件:最优子结构和子问题的重叠性质。从一般意义上来说,这两个性质是用来动态规划求解的基本要素。
(1)、最优子结构性质:当问题的最优解包含了其子问题的最优解时,称为该问题具有最优子结构性质。在分析问题的这个性质时,所用的方法具有普遍性。首先假设由问题最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解的更好的解,从而导致矛盾。一般采用自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。
(2)、重叠子问题性质:在用递归方法自顶向下求解问题时,每次所产生的子问题并不是新的问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每个子问题只计算一次,然后将其保存在一个表格中,以方便以后用到的时候查询,从而能获得较高的解题效率。
在从计算矩阵连乘积最优计算次序的动态规划算法中可以看出,该算法的有效性依赖于问题本身所具有的两个重要性质:最优子结构性质和子问题重叠性质。从一般意义上讲,问题所具有的这两个重要性质是该问题可用动态规划算法求解的基本要素。这对于在设计求解具体问题的算法时,是否选择动态规划算法具有指导意义。设计动态规划算法的第一步通常是要刻画最优解的结构。当问题的最优解包含其子问题的最优解时,称该问题具有最优子结构性质。问题的最优解子结构性质提供了该问题可用动态规划求解的重要线索。在动态规划算法中,利用问题的最优子结构性质,以自底向上的方法递归地从子问题的最优解逐步构造出整个问题的最优解,使我们能在相对小的子问题空间上考虑问题。例如,在矩阵连乘积最优计算次序问题中,子问题空间由矩阵链的所有不同子链组成。所有不同子链的个数为O(n2),因而子问题的空间规模为O(n2)。可用动态规划算法求解的问题应具备的另一基本要素是子问题的重叠性质。在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。多态规划算法正是利用了这种子问题的重叠性质,对每个子问题只解一次,而再次需要解此子问题使,只是简单地用常数时间查看一下结果。通常,不同的子问题个数呈多项式增长。因此,用动态规划算法通常只需多项式时间,从而得到较高的解题效率。最后一个专题是图算法。介绍一下关于最小生成树的问题,求解最小生成树主要有两种方法,即prim方法和Kruskal算法。
Prim算法基于一种贪心的思想,通过局部最优策略,每次将一条边加入所构建的生成树中,加完n-1条边后,保证最后所得的生成树是整体最优的,即最小生成树。下面简单说明下Prim算法的步骤:
Prim算法将图中的每个点分成三种状态:第一种tree点,表示该点已经在所构造的生成树中。第二种fringe点,表示还没在树中,但是和Tree点相邻(有一条直接相连),是即将要加入生成树中的候选点。第三种unseen点,其他的点,表示还没有检测到的点。那么Prim算法步骤为:
1:初始时将所有点初始化成unseen,并且随机将一个点S设为tree点(如1号点)。
2:将所有与S相邻的点设为fringe。
3:如果图中还存在fringe点,则到4,否则到6。
4:在所有连接一个fringe点和一个tree点的边中,取权值最小的边e(u,v),不妨设u为tree点,v为fringe点,将v点设为tree点。
5:将所有与v相邻的unseen点设为fringe,并将e加入到构建的生成树中。转到3。
6 : 如果图中所有点均为tree点,则图的一个最小生成树找到。否则原图不存在最小生成树。
7:算法结束。
Kruskal算法同样是基于贪心策略,但是它和Prim算法不同的是在算法过程中它并不维护一个连通的分量,而是将多个连通分量合并到一起得到一颗生成树。个人觉得Kruskal算法的思想比它算法本身要重要,能够解决很多其他问题,至少是在ACM比赛中吧。下面描述一下Kruskal算法的步骤:
首先将图中的所有的边按照其权值从小到大排序,然后从小到大枚举边。设需要构造的生成树为T。
1:初始时,T中没有变,所有点看成是一个单独的集合。
2:如果枚举完最后一条边,则到4,否则到3。
3:设当前枚举的边为e(u,v),如果u,v不在同一个集合中,则将e加入到T中,并且将u和v合并到一个集合中,否则,忽略e。转到2。
4:如果T中含有n-1条边,则说明找到原图的最小生成树,否则说明原图没有最小生成树。算法结束。关于acm的课程内容总结到这里就差不多了,下面我就好好谈谈关于acm做题的一些感悟,希望能对你有一定帮助。首先要提到的,也是最次要的,是我的编程能力得到了极大的提升。之所以说次要,是因为如果以后不做CS相关的事情,这个东西作用不大,而且适合我的经验也不一定适合别人。第二点,是我读程序的能力大幅提升。我在读程序的时候在想:如果我写这个题要怎么写?别人的程序哪里和我想的不一样?他为什么那样写?是不是有问题?我写这个题有哪些细节是需要注意的?他的细节都处理对了么?等等。把这些问题都回答完,很难找不出错在哪里来。当然这里唯一需要能力的,就是自己写程序的水平一定要与写这个程序的人的水平相当,不然实在理解不了他在写什么,一切都是白搭。读程序除了帮别人排错这一点以外还有别一个好处,那就是能学习到别人代码里优秀的东西。因为你会想:为什么他这里这么写?他的写法和我的写法比好在哪里?差在哪里?于是就可以总结出最好的东西。
Acm就这样结束了,但对我的影响很大,使我得到了提升,感谢acm!