ACM总结报告
算法设计
姓名:杨加勇
学号:20143617
专业:计算机2班
指导老师:费玉奎
说是报告,其实更算是个总结,这学期这门选修课所学知识的一个大致总结。还是开始选择这门课时所说的,并无后悔或者惋惜一说,算法或许在以后一点也用不到,这个只是或许一直荒废,我不考研,也不去参加什么竞赛,工作也只是将关于计算机的作为一个过渡,我只想去创业,我只想管理自己创造出来的小小财富或是团队或是公司,但我相信,算法所设计的策略思维模式,与其他管理等决策的问题的解决方法,都有异曲同工之妙,生活没有生搬硬套的答案,只要自己有过那么一闪而过的思维模式,我想解决问题只是时间罢了。
上这门课我确实也没有失望,收获了很多。开始上的时候觉得16周很长,转眼4个专题一过,16周已然飘过,而这门选修课也告一段落。对于这门课所学,猛的一想,竟然感觉没有什么印象,细细想来,一些思想,一些方法,一些策略,不知觉就遍布脑海。
首先的专题就是贪心算法了,贪心,顾名思义,就是贪婪的意思,在规则之内找到最佳的解决方案。按照老师的定义来说:在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。回顾来看,贪心算是最简单的了,题目也有很多水题。一开始我做的时候根本不熟练,整个一小白,看了例题一遍又一遍,做题都是按照老师给的套路生搬硬套进去解题的。后来慢慢明白了算法的核心,也渐渐会用了。贪心不是从整体上解决问题,贪心算法所作出的最优解只是局部的最优解,而由于问题本身的特性,利用贪心算法所作出的由局部的分解的最优整合在一起即为整体的最优值了。对于贪心算法的选择,其实需要一对“火眼金睛”,其实也考研说是对贪心的理解,直接可以看透问题的本质,判断贪心算法适合不适合来运用解决这题,给直接留点时间判断思考,一旦确定,那么其实代码就清晰可见了。
贪心算法,不仅对于代码题,我觉得做事也是如此,如果你有很远大的目标,一旦确定,分解为小的目标,如何一个个的通过最好的方法去实现它,最后可能是到达梦想的最快途径。其他方面应该也和贪心算法有着莫名的相似性,总之,贪心算法,不仅仅是算法而已,更重要的是思维方式和策略模式。
以下是贪心的简单的示例代码:
//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;}
第二个专题就是搜索了,很系统的分为深度搜索和广度搜索了。我在大一时候做题吧,经常遇到地图的题目,什么迷宫啊,在地图上拣钱啊,逃出升天啊,在最短时间内救出公主啊等等啥的,那都是天文感觉,完全摸不着头脑,根本不知道如何下手。而搜索专题就是针对地图来的就好像,把一个个问题分解,如何逐一解决,最后找到答案,虽然很繁琐,但确实把答案找到了,而且经过长时间的接触地图类的题目,感觉其实都是千篇一律,都有套路在里面,没什么新鲜感了,唯一有的说的,就是根据题目本身的意思来增加一系列的枝丫限制了,达到搜索出答案的目的。
先说说深度搜索吧,根据经验,其实最实用,最普遍,最简单的应该就是它了。它从某个状态开始,如何根据题意创建函数来递归。它的效率其实不是很高,因为递归调用本身就本身很高,但深度搜索为了弥补这一缺点,可以根据题意通过剪枝限制条件来限制一些在一开始看起来就没有答案的路径,直接给判个死刑,避免再继续沿这条道路走下去而浪费时间了。搜索这类题目,隐蔽性很低的,小白都可能知道你这是考的啥,用深度搜索或者广度搜索来解决,直接套路就OK了。那么难点在哪里呢?题目不可能会那么简单的,否则还考你啥呀,直接给你分数不是更直接。地图题目考搜索考的是剪枝!对,就是对于题目本身的了解来对搜索方法进行限制,如果限制的不到位,很大可能直接超时,通过你的代码,考研找到答案,只是时间要的久点罢了,那也是失败的。最最关键的是对题目的把握,找到题目的隐含条件,来增加剪枝限制,减少时间的消耗。这点是最关键的了。
再谈谈广度搜索吧,广度搜索和队列相配合,比深度搜索要麻烦点,而且也不太实用,限制性比较大,主要在题目问有没有最优,最小,最多,最合理什么的用到这个方法,而且套路也很固定。主要是取状态,看是否合法,合法就加入队列,然后取头,删头,判断下一状态,合法再加入等等,直到队列为空为止。
地图的题目有的看不出来需要用深度搜索还是广度搜索,就有可能都可以,或者隐蔽性好,首先先选择深度搜索,比较简单,也普遍实用,如果不能实现问题,那么再换广度搜索也不迟。其实在我看来,广度搜索也是一种另类的递归调用,只是比较隐蔽,用队列来隐藏了本来面目。深度搜索和广度搜索,是两种方法的不同道路的搜索,就像一千个读者就有一千个哈默雷特一样,不同的人搜索地图的时候不一定次序相同,但结果都是一样的,主要是是看根据实际来选择最优的方法罢了。人都有惰性,选择最好的最省事的那个才是最佳的了吧。
地图搜索在现实中,类似于满大街找人,看你选择的方法怎样最省事的找到就行。地图搜索见的比较多的,应该在游戏里,许多任务都是走迷宫找宝藏什么的。或许还有更大的发现类似于地图搜索在等着我去寻找,我想既然拥有了搜索思维,以后总会有它的用武之地,艺多而不压身。
以下是广度搜索的示例框架:
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
以下是深度搜索的示例框架:
递归实现:
Function Dfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs (Step + 1, Next ))
End
非递归实现:
While Not Stack.Empty ()
Begin
Tmp = Stack.top()
从Tmp拓展下一个未拓展的状态Next
If 没有未拓展状态(到达叶节点) Then
Stack.pop()
Else If 状态Next合法 Then
Stack.push(Next)
End
第三个专题就是动态规划了。其实这个专题的时候,做题很是矛盾。怎么说呢,简单题,水的一塌糊涂,难题,看的两眼直发呆,对,就是这两种极端。我记得老师讲动态规划的时候是以最长上升子序列入手,然后是各种改头换面的上升子序列问题,那节课的结尾是以最简单的一种背包问题收尾的。再下节课就是各种的背包问题了,我当时已然懵了,都只顾看代码了,都没怎么注意题名,现在都记不太清楚具体的什么背包问题了。其实上动态规划完,我是兴奋的,第一次发现数组可以那样子用,感觉一下子刷新了我对数组的认识一样。以前二维数组,都是表示个表啊,地图啥的,都是二维平面的东西,老师讲的将2个下标分解开用,感觉很是新鲜,解决这类问题也确实收到了奇效,当然,也对像我这样的初学者不仅对于数组,一定还有一些我未发现的直接常用的事物的不同用法,我深信它们存在,这对于初学者来说是个考研,也是福音,多去发现,可能不知不觉直接已经是大神级别的编程师了。
动态规划,难在规划上。对于专题内的题目来说,迷惑性趋向于0。但在专题外,就像老师说的,看完题目,很难向动态规划上去想。所以会觉得很难很难,但一旦想到可能是动态规划的问题,那么基本上答案就呼之欲出了。动态规划是难,难在题干,难在迷惑性强。动态规划又很简单,单一的抽象,单一的方法,单一的循环等等,所以可以这么说:如果一道迷惑性很强的动态规划题,你恰好想到了可能是动态规划并动手去试,那么其实你已经把这题解决了。动态规划拥有很固定的套路,只要按照套路,然后再根据题意修改一下细节,完全是没有难度的。
动态规划在现实中是无处不在的,否则题目也不会有那么强的迷惑性。在现实中解决问题时候,一旦感觉无从下手,试试动态规划的策略模式,往往可能一击中敌,得到答案。
动态规划问题一般的递推关系式:
F[a][b]=max(F[a-1][b],F[a][b-1])+Coin[a][b]
(此步即为递归定义最优解的值,列出状态转移方程)
动态规划问题的一般解题步骤
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
最长上升子序列例题的示例代码如下:
$include<iostream>
Uisng namespace std;
int b[MAX_N + 10];
int aMaxLen[MAX_N + 10];
int main()
{
int i, j, N;
scanf("%d", & N);
for( i = 1;i <= N;i ++ )
scanf("%d", & b[i]);
aMaxLen[1] = 1;
for( i = 2; i <= N; i ++ )
{ //求以第i 个数为终点的最长上升子序列的长度
int nTmp = 0; //记录第i 个数左边子序列最大长度
for( j = 1; j < i; j ++ )
{ //搜索以第i 个数左边数为终点的最长上升子序列长度
if( b[i] > b[j] )
{
if( nTmp < aMaxLen[j] )
nTmp = aMaxLen[j];
}
}
aMaxLen[i] = nTmp + 1;
}
int nMax = -1;
for( i = 1;i <= N;i ++ )
if( nMax < aMaxLen[i])
nMax = aMaxLen[i];
printf("%d\n", nMax);
return 0;
}
(主要是递推关系式,做题模版很容易被套用,而后求解)
第四个专题就是图论了。实话说,第四个专题我感觉不是一般的难,虽然有模版有算法,解题难度还是很大。首先,老师讲的是图中边和点的存储方法,一个是利用二维数组的邻接矩阵的方式,但限制是点不能很多的,边可以稠密的图。另一个是邻接表,标准的应该是用指针式的链表存储,为了方便和运行速度,修改成结构体的数组来存储,运行速度要快,而且更加方便,它的限制就是边不要太多,点数可以很多的图。而后引出了并查集,就是相同的元素放在一个集合里,用数组表示。当然,这里面有合并集合和查找数值操作,都有固定的模版方法。
下面的才是主菜,一共两道。第一道主菜是最小生成树。最小生成树的定义:所有生成树中权值最小的一个边集T为最小生成树,确定树T的问题成为最小生成树问题。解决问题的方法有两种,一个是prim算法:任取一个顶点加入生成树;在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。重复上一步骤,直到所有的顶点都进入了生成树为止。一个是kruskal算法:对所有边从小到大排序;依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;直到生成树中有了n-1条边,即告终止。算法的时间复杂度O(eloge)。其中prim算法简单解释起来就是有2个集合,一个是已取的点集合,一个是未取的点集合,根据边的权值来从未取的集合中找点,而后将此点放到已取的集合中,直到未取的集合为空为止。看似很简单的操作,实际注意的细节有很多,最主要的是根据已取点集合的点与未取点集合的点的关系来找权值,而后才能进行合并操作。对于kruskal算法来说就比较单一了,首先就是看边的权值,让权值按从大到小排列,然后取相对最小的边,加入,判断,直到生成联通图为止。
第二道主菜就是最短路问题了,简单来说就是在地图上找路线,从出发点到达目的地的最佳的路线。一种算法是Dijkstra算法:设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,对vi∈V-S,假设从源点v到vi的有向边为最短路径。以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。这算法和最小生成树的prim算法异曲同工,只是所求不同。Dijkstra算法有个很大的缺点就是如果权值是负值,那么就不能够实现,所以一定要因题选择。另一个算法是Bellman-Ford算法:Bellman-Ford算法构造一个最短路径长度数组序列dist
1 [u], dist 2 [u], …, dist n-1 [u]。其中:
dist 1 [u]为从源点v到终点u的只经过一条边的最短路径长度,并有dist 1 [u] =Edge[v][u];
dist 2 [u]为从源点v最多经过两条边到达终点u的最短路径长度;
dist 3 [u]为从源点v出发最多经过不构成负权值回路的三条边到达终点u的最短路径长度;
……
dist n-1 [u]为从源点v出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度;
算法的最终目的是计算出dist n-1 [u],为源点v到顶点u的最短路径长度。
Dijkstra算法与Bellman算法的区别:Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,修改的仅仅是源点到T集合中各顶点的最短路径长度。Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[ ],也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。
还有一种SPFA算法,算是Bellman算法的优化实现,一般比较常用:1.队列Q={s}2.取出队头u,枚举所有的u的临边.若d(v)>d(u)+w(u,v)则改进 ,pre(v)=u,由于d(v)减少了,v可能在以后改进其他的点,所以若v不在Q中,则将v入队。3.一直迭代2,直到队列Q为空(正常结束),或有的点的入队次数>=n(含有负圈)。
一般用于找负圈(效率高于Bellman-Ford),稀疏图的最短路。
这几种算法都有模版代码,但相对于前几讲的,还是很复杂的,尤其是根据题目还有实现限制一些细节,那更不用说了,也许是自己刚接触的缘故,不怎么熟悉,我想以后多做题应该就能够彻底掌握,刚学的知识都应该有个沉淀的过程。
并查集问题的示例代码:
find3(x)
{ r = x;while (set[r] <> r) //循环结束,则找到根节点
r = set[r]; i = x;
while (i <> r) //本循环修改查找路径中所有节点{
j = set[i];
set[i] = r;
i = j;}
}
Dijkstra算法——伪代码如下:
1. 初始化数组dist、path和s;
2. while (s中的元素个数<n)
2.1 在dist[n]中求最小值,其下标为k;
2.2 输出dist[j]和path[j];
2.3 修改数组dist和path;
2.4 将顶点vk添加到数组s中;
总结:业精于勤,荒于嬉;行成于思,毁于随。感谢这学期费玉奎老师的谆谆教导,也祝愿自己以后的路越走越远。