标签:
ACM课程总结
一、概述
紧张却又充实的整个学期就要过去了,而ACM的最后一个专题,图论专题也已经临近关闭。在ACM的短暂生涯里,我从费老那里学到了很多知识。我知道了贪心算法是什么,懂得了需要脑洞大开的动态规划,以及搜索(BFS和DFS)思想、图论中的最小生成树,运用并查集求出最小连通图。算法,可以说,想它难它就让你痛苦;想它容易,那么A题的快乐也就随之而来。依稀记得第一堂课,费老强调了不止一次,“ACM不是一门容易的课程,并且它的学分是所有选修课里面最高的,如果你们当中又人存在混学分的侥幸心理,那么,我劝你们尽早退出。这样就没意思了。ACM,难,很容易坚持下来,如果自己不能吃苦的话,趁早退。”的确如此,在第一堂课结束之后,我身边就有朋友找到了教务处,死活也要把ACM课程退掉。他这样告诉我,“ACM,太难了,这简直就没有办法坚持吗。”我很庆幸,在四个专题的训练中存活了下来,对当初的决定丝毫不会后悔。困难,就是生来被克服的。我想,我在整个学期的训练中,克服了重重困难,虽然没有一个专题的题目全部做满,但是,这依然是一段宝贵的经历。
二、贪心算法
从贪心算法讲起,“在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解”,这就是费老对于贪心算法的定义。下面给出贪心算法的伪代码:
//A是问题的输入集合即候选集合
Greedy(A)
{
S={ }; //初始解集合为空集
while (not solution(S)) //集合S没有构成问题的一个解
{
x = select(A); //在候选集合A中做贪心选择
if feasible(S, x) //判断集合S中加入x后的解是否可行
S = S+{x};
A = A-{x};
}
return S;
}
贪心,顾名思义,从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。这是书上给出的贪心算法的一大特性。也正是由于这一特性,我才在专题一的训练之中可以对那些基本题目游刃有余。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一,这同样是毋庸置疑的。
2.1关于经典例题
对于贪心算法,大致的分为以下几类:活动安排问题,背包问题。其中的背包问题有可以细分为0-1背包问题,多重背包问题和可分割的背包问题,但是只有最后一种背包问题才是运用贪心算法来解决的,前面的两种背包问题在第三专题动态规划中会有更为细致的研究。浏览着老师讲过的课件,仿佛每一个例题都是那么熟悉。活动安排问题,教会了我先把所给的活动按照时间顺序来进行排序,然后从集合中选出最大的相容活动子集合。贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。接下来是从走廊移动桌子的例题,这可是我A的第一道题,也是入门的关键,花费的时间也不是一星半点,用了整整一周的时间去完成对题目的理解和算法。我尝试贪心算法去解决问题,结果却不如人意,但是我确定算法的思路没有出现一点纰漏。所以,在我最后用另外一种方法去解决问题之后,贪心选择的代码我也放到了博客的最下面。最后的经典例题就是约翰去人工湖钓鱼的题目,在课堂上听得确实是云里雾里,但是课后仔细一想,把复杂问题细化之后,也没有那么难已解决。最后的例题的贪心策略是最重要的。采用贪心策略,每次选择鱼最多的湖钓一次鱼,对于每个湖来说,由于在任何时候鱼的数目只和约翰在该湖里钓鱼的次数有关,和钓鱼的总次数无关,所以这个策略是最优的。一共可以钓鱼time次,每次在n个湖中选择鱼最多的一个湖钓鱼。老师也强调过,如果把最难的题目可以融汇贯通,那么还有什么是不可以解决的呢?
2.2收获
在这段时间,我知道了Runtime error基本上是因为数组越界或者未赋初值。并且,time exceed limit是因为多加了一个while语句或者for循环未成功退出。总而言之,万事开头难,我收获了很多。贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。利用贪心策略解题,需要解决两个问题:该题是否适合于用贪心策略求解;如何选择贪心标准,以得到问题的最优/较优解。懂得了排序函数,把每一个复杂的问题转化为一个又一个的简单问题,进而在每一步求出最优解,最优解在可行性检查完之后,就是整个问题的最优解。这,应该就是我最为珍贵的贪心算法模板。
三、搜索算法
搜索算法,顾名思义,是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。相比于单纯的枚举算法有了一定的方向性和目标性。算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态)。当然,理解状态和状态转移的含义尤为重要。状态(state)是对问题在某一时刻进展情况的数学描述,或者是数学抽象。每一个状态都会是答案的一个“可能的”解。状态的转移就是问题从一个状态转移到另一个状态,这样就可以进行搜索的一步步延伸,最后要得到的解也是其中的一个状态。搜索分为广度搜索(BFS)和深度搜索(DFS)。从BFS讲起。
3.1广度搜索伪代码:
While Not Queue.Empty ()
Begin
可加结束条件
Tmp = Queue.Top ()
从Tmp循环拓展下一个状态Next
If 状态Next合法 Then
Begin
生成新状态Next
Next.Step = Tmp.Step + 1
Queue.Pushback (Next)
End
Queue.Pop ()
End;
3.1.1广度优先搜索思想:
书上给出的基本思想是:从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。确实如此,广度优先搜索只能如此。值得注意的是,广度优先搜索是把每一层的状态遍历,递归向下。所以使用队列是最符合这种思想的数据结构。利用队列先进先出(FIFO)的性质恰好可以来完成这个任务。具体过程:每次取出队列首元素(初始状态),进行拓展;然后把拓展所得到的可行状态都放到队列里面;将初始状态删除;一直进行以上三步直到队列为空。
3.1.2广度优先搜索的经典例题:
在第二个专题的训练中,遇到过许许多多的经典。有一个奇怪的电梯,在每一层只能上升或下降一个特定的数字,中间不会停止。现在要求写出一个程序,求出可不可以到达指定的楼层,可以的话输出次数,否则return -1。这是广搜的基本算法,写出BFS函数,一切就会迎刃而解。还有非常可乐的问题,相信没有人会忘记。大家一定觉的运动以后喝可乐是一件很惬意的事情,但是seeyou却不这么认为。因为每次当seeyou买了可乐以后,阿牛就要求和seeyou一起分享这一瓶可乐,而且一定要喝的和seeyou一样多。现在只有两个杯子,它们的容量分别是N毫升和M毫升,可乐的体积为S (S<101)毫升 (正好装满一瓶) ,它们三个之间可以相互倒可乐 (都是没有刻度的,且 S==N+M,101>S>0,N>0,M>0) 。现在需要写出一个程序,尝试求出可乐能否被平分。这是老师上课讲过的一个例题,虽然上课听了个懵懵懂懂,但是,花了2、3个晚上,终于融会贯通了。本题运用广度搜索算法BFS(),复杂的是对各种情况的考虑。细心、耐心才能把每一种情况完整地考虑在内,这时候核心的不是广度搜索算法了,而是对每一种情况的考虑。这也正是ACM的魅力所在,是她神奇的地方。有时候,你去考虑一个问题,知道该用什么算法去求解,但是有很多算法之外的知识,吸引人的也就不是广搜算法本身了,而是基于广度优先搜索的队各种情况的细致考虑。回想专题二,这可能就是我记忆最为深刻的经典难题。从杯子把可乐倒入另一个杯子,或者倒回可乐瓶,把每一种情况写成代码,就是答案。
3.2深度优先搜索:
深度优先搜索伪代码:
递归实现:
Function Dfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs (Step + 1, Next ))
End;
3.2.1深度优先搜索的基本思想:
基本思想:从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。DFS的过程,状态必须在遍历完所有它的子状态之后,才能继续进行对同一层中下一个状态的遍历。而这一种特质,正巧符合堆栈的数据结构思想。先进后出(FILO)的性质,完美契合了DFS的算法。具体实现过程:每次取出栈顶元素,对其进行拓展;若栈顶元素无法继续拓展,则将其从栈中弹出。继续1过程;不断重复直到获得目标状态(取得可行解)或栈为空(无解)。遍历到每一个状态的最深层次,然后回溯,正是深度优先搜索的关键。
3.2.2深度优先搜索的经典例题:
在ACM中,有著名的N皇后问题,这同样也是在专题二的训练中存在的题目。N皇后是典型的dfs深搜题目,写出深搜算法就得以实现了。也就是考虑皇后放置的位置。对于每一行,需要枚举每个可以放置皇后的位置,并且需要判断当前位置(第i行)是否满足条件(即判断这个位置是否与放置好的前i-1行的皇后的位置相冲突)。如果冲突,则位置不合适;否则的话,就可以枚举下一行皇后的位置,直至问题的结束。
透过经典,我看到了深搜在实际问题中的广泛应用。还有石油问题,不能复制的经典。一个m*n的地图,其中的格子要么是*,要么是@,对于@,横竖斜着的成为一个块,现在要求编写一个程序求出总共有多少@块。最最经典的深搜题目。老师在课上也讲过,所以思路自然而然的就有了。设置visited[]数组和方向数组,因为可以向八个方向深搜,所以用一个二维数组表示出来。判别边界和是否被访问过,之后dfs函数写出来,一切就都结束了。课上的例题也是最经典的一部分。对于经典,本就是不可重现的模板。
3.3收获:
在本专题中,有令人熟悉的二分三分算法,还有让人头大的深搜以及广搜。总体来说,可以掌握基本的内容。在今后的时光里,必须继续加强思维的锻炼并且逐步地去提高自己。搜索的那些经典例题,不得不说,一个什么都不会的人,从中都可以受益匪浅。有了经典在记忆中,看见没做过的题目就可以往这样那样的模板里去想。如果正巧是DFS或者BFS,那么就能一步一步地得出最终的结果。搜索把每一层的状态给枚举出来,根据一定的条件,如果不符合,则回溯到上一层,继续相同的工作,直至得到最优解。这种思想,会在我的心中生根发芽,在解决问题中起着不可替代的作用。
四、动态规划:
4.1动态规划的基本思想:
动态规划是解决多阶段决策问题的一种方法。也就是说,如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果。如果不仔细去理解动态规划的涵义,似乎和贪心有着异曲同工之处,但不尽相同。不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。最优决策序列的子序列,一定是局部最优决策子序列。包含有非局部最优的决策子序列,一定不是最优决策序列。并且,在做每一步决策时,列出各种可能的局部解,依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。以每一步都是最优的来保证全局是最优的。
4.2 步骤
因为动态规划的具体问题,状态转移方程也不尽相同,所以只给出老师所说的步骤:判断问题是否具有最优子结构性质,若不具备则不能用动态规划;把问题分成若干个子问题(分阶段);建立状态转移方程(递推公式);找出边界条件;将已知边界值带入方程;递推求解。难点就是状态转移方程的建立,以及递推的应用。
4.3经典例题和收获:
动态规划的工作的种类,有01背包问题、多重背包问题、组间01背包问题、组间多重背包问题,当然还有背包问题的优化,以及区间的动态规划。这些问题的状态方程都是简洁明了,意思都能很好的理解。但是,有一个问题就是不知道什么时候去使用。相信在之后的生涯中,越练愈多,能够慧眼识类别。递归调用和状态方程的建立是专题三的核心。当然,状态方程还可以用画图的方式求出来,记得很清楚的是一个接馅饼的题目。可以画出来一个表格,然后用一填充馅饼,从后向前递归,最后就能得到结果。找规律也是一个不错的选择,有很多题目,写出来前几项,状态方程立马就有了。解决最最核心的问题,动态规划看起来似乎就没有那么难了。
五、图论
5.1图的几种算法:
在图这一个专题中,有最小生成子树的Prim算法和kruskal算法。并且引入并查集的概念。Prim的基本思想:任取一个顶点加入生成树;在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。重复上一步骤,直到所有的顶点都进入了生成树为止。而kruskal算法将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。最终得到的结果就是最小生成树。
并且,在另一方面,还有最短路径问题,Dijkstra算法和Bellman-Ford算法、spfa算法、floyd算法。Spfa算法和kruskal算法一样,是实际问题中最常用的算法。其他的算法,是理想状态下的思路,但对于规模比较大的问题,并不适用。
5.2例题和收获:
在本专题中,有着很多内核相同的题目。把顶点和边的权值对应到代码中,求出最小生成子树或者运用并查集求出最小连通子图、最短路径,正是图的内涵所在。而且,各种算法大都以贪心为基础,对边的权值排序,然后运用并查集进行处理。prim算法用于稠密图,因为算法本身是从两个不连通的集合选取边最小的点,最终得到最小生成树;kruskal算法用于稀疏图,因为算法原则是选边,如果边数太多的话,并不适用。然后是求出最短路径的算法,SPFA 其实就是Bellman-Ford的一种队列实现,减少了冗余,即松驰的边至少不会以一个d为∞的点为起点。松弛技术是这些算法的关键所在,其伪代码如下:
Relax(u,v,w)
if(d[v]>d[u]+w(u,v))
d[v]=d[u]+w(u,v);
数据结构中同样讲述过图论,但是并没有像ACM这样面向实际去解决问题。拥有了这些思想,把问题向这些模板靠拢,进而写出代码,解决问题,是图论专题留给我的最大收获。
六、总结
ACM课程结束了,但对于算法知识的学习之路还远远没有结束。因为如此,相信在今后对于程序的处理中会运用所学,诞生更好的方式。
标签:
原文地址:http://blog.csdn.net/tansanity/article/details/51803586