ACM总结报告!
Posted 默默无闻的程序yuan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM总结报告!相关的知识,希望对你有一定的参考价值。
ACM总结报告
算法设计
姓名:郭嘉
学号:2015590
专业:网络工程二班
指导老师:费玉奎。
第一次听说ACM这个事情是我们的计算机导论老师岳训老师介绍给我的,他可以算是我计算机的启蒙老师,带我走进了计算机这个大世界。他让我知道了一个优秀的程序员就像是一位“武林高手”一样,需要掌握 “内功”和“外功”,外功指的就是程序设计语言,比如C、C++、Java、Python。而内功就是指算法,数据结构,设计模式等等。这“内功”和“外功”缺一不可。他也告诉我人生在世不能心急,要要懂得技巧,有一些程序员,只顾着去学习“外功”,而不去修炼“内功”,其结果就是虽然掌握了许多时髦的程序设计语言,但是却写不出好的程序,亦或是只会简单的重复别人写的包和库,而不会自己去创造,他又跟我说了有些时候一定要抓住时机,向前一步就能走向成功,而在某一次课间的时候,他给我说了一个机会,他说费老师的ACM对你来说的一个挑战也是一个机会,选不选择看你,这个对你来说真的很不错,你可以参考一下,当然我绝对不会逼迫你什么,全凭你自己。听了岳老师的一番话语之后,我回去立刻查了什么是ACM和ACM的相关知识知道了ACM国际大学生程序设计竞赛(英文全称:ACM International Collegiate Programming Contest(简称ACM-ICPC或ICPC))是由美国计算机协会(ACM)主办的,一项旨在展示大学生创新能力、团队精神和在压力下编写程序、分析和解决问题能力的年度竞赛。ACM-ICPC以团队的形式代表各学校参赛,每队由至多3名队员组成。每位队员必须是在校学生,有一定的年龄限制,并且每年最多可以参加2站区域选拔赛近年来,我国各大高校逐渐开始重视ACM程序设计竞赛,每年的Final总决赛上都会出现我国选手的影子。以上海交通大学、清华大学、北京大学、浙江大学、复旦大学为代表的我国高校屡次在总决赛中捧杯,为我国争取了荣誉,也在世界上确立我我国的地位。
而我又了解到,我校作为山东省重点省属高校之一,ACM水平在省内维持着前五的水平,虽为农业大学,但是在这种理工科的竞赛中也拥有绝对的话语权和地位。又通过学长和计算机,数学专业的同学了解到,费老师作为计算机系的正教授,有自己的讲课风格和教学形式,更注重内在的教学,教学不拘一格,他的课程,可以说非常的难,但是就像岳老师说的那样,绝对是一个机会也是一个挑战,在了解了这一切后,我别无选择,只有前进!
我选择了这门课,为的就是锻炼自己的算法能力以及思考能力,探索解决问题的方法,了解一个程序运行的本质和内在机理以及支撑它的框架。从而真正的把程序握在自己的手中。或许自己的能力不够,学到的东西也比较少,但是我觉得上这门课我确实也没有失望,收获了很多。开始上的时候觉得16周很长,转眼4个专题一过,16周已然飘过,而这门选修课也告一段落。对于这门课所学,细细想来,一些思想,一些方法,一些策略,不知觉就遍布脑海。
1.贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
从我的理解来看,目前遇到的贪心问题分为两种:背包问题,区间问题。
背包问题属于比较简单的那种类型,上课老师讲的例题就是这种类型,这类问题要么以物品数量为标准,要么以性价比为标准,通过一次For循环即可求解。用贪心法求解背包问题的关键是如何选定贪心策略,使得按照一定的顺序选择每个物品,并尽可能的装入背包,知道背包装满。至少有三种看似合适的贪心策略。
(一)选择价值最大的物品,因为这可以尽可能快的增加背包的总价值,但是,虽然每一步选择获得了背包价值的极大增长,但背包容量却可能消耗的太快,使得装入背包的物品个数减少,从而不能保证目标函数达到最大。
(二)选择重量最轻的物品,因为这可以装入尽可能多的物品,从而增加背包的总价值。但是,虽然每一步选择使背包的容量消耗的慢了,但背包的价值却没能保证迅速的增长,从而不能保证目标函数达到最大。
(三)以上两种贪心策略或者只考虑背包价值的增长,或者只考虑背包容量的消耗,而为了求得背包问题的最优解,需要在背包价值增长和背包容量消耗二者之间寻找平衡。正确的贪心策略是选择单位重量价值最大的物品。
区间问题是一个难点,ProblemA等题是这类问题的代表,区间问题的特点是较难找出一个求最优解的标准,标准找不出,问题就很难解决,以ProblemA为例,解题的关键是理解串行和并行,并将通过次数最多的路段的总时间作为最优解。
以下是贪心的简单的示例代码:
//A是问题的输入集合即候选集合
Greedy(A)
{
S={ }; //初始解集合为空集
while(not solution(S)) //集合S没有构成问题的一个解 {
x = select(A); //在候选集合A中做贪心选择
iffeasible(S, x) //判断集合S中加入x后的解是否可行 S =S+{x};
A =A-{x};}return S;}
2.搜索
广义的搜索算法分为四个大类,二分查找算法(Binary-Search),三分搜索算法(Ternary search),深度优先搜索算法(DFS),广度优先搜索算法(BFS)。前两个是针对给定公式的求解思路,后两个是针对给定图形的求解思路。
(一)二分查找算法:
二分查找主要针对的是单调函数给定函数值,求自变量值的情况,非常简单,优点是比较次数少,查找速度快,平均性能好,二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x.。这里需要注意的是,不一定非单调函数就不能用二分,有时结合求导。可以求出函数单调性,从而求极值。例如1002题,需要先对给定函数求导,然后再二分,从而求解。
(二)三分搜索算法:
三分查找主要针对的是凸性函数给定函数值, 求自变量值的情况,难道较大,是在二分的基础上,对某一区间再次二分的一种算法。已知左右端点L、R,要求找到白点的位置。
思路:通过不断缩小 [L,R] 的范围,无限逼近白点。
做法:先取 [L,R] 的中点 mid,再取[mid,R] 的中点 mmid,通过比较 f(mid) 与 f(mmid) 的大小来缩小范围。当最后 L=R-1 时,再比较下这两个点的值,我们就找到了解。
(三)DFS:
深度优先算法属于图算法的一种,其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
深度优先遍历图的方法是,从图中某顶点v出发:
(1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
原来对DFS的理解仅仅局限于图,现在发现这只是最基础的。DFS更多的表示的是一种状态,然后利用某中很简单的思维进行一次次的尝试,每次尝试成功了,就深入一层递归进行下一次尝试,直到之后的尝试表明已经失败了不会成功,则回溯到这里。取消这次的尝试,去尝试其他的操作。简单地说,就是暴搜。只不过利用了递归来实现尝试失败时的回溯,从而进行新的尝试。
(四)BFS:
从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中,每次取出队首元素进行检验,如果符合条件,就停止,如果不符合就剔除掉,循环检验,知道队列为空位置。BFS在求解最短路径或者最短步数上有很多的应用。应用最多的是在走迷宫上,例如本专题的国际象棋问题,就是使用了此种思路。
以下是广度搜索的示例框架:
WhileNot 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
以下是深度搜索的示例框架:
递归实现:
FunctionDfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs(Step + 1, Next ))
End
非递归实现:
WhileNot Stack.Empty ()
Begin
Tmp= Stack.top()
从Tmp拓展下一个未拓展的状态Next
If 没有未拓展状态(到达叶节点) Then
Stack.pop()
ElseIf 状态Next合法 Then
Stack.push(Next)
End
3.动态规划
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。
动态规划问题运算量比较大,通常提前列出问题的所有可能并保存到表中,在根据键值查表,这种方法的特点是牺牲空间来换取时间,举个简单的例子,斐波拉契数列,求f(10),需要11次运算,求f(11),需要12次运算,如果用公式f(n) = f(n-1) + f(n-2)分开求,需要计算11+12=23次,但如果将将f(10)保存,计算f(11),只需在f(10)的基础上再运算一次,即11+1=12次。可见,这种做法非常节省时间。
动态规划问题,相较于贪心和搜索来说,一个显著的特点就是代码量很少,一个贪心或者搜索问题,一般都会有五六十行代码,而一个DP问题的代码量,往往就十几行甚至更短,以至于真正的核心就那么一行递推公式。但这并不代表DP问题就简单,做DP问题需要很强的逻辑思维和思考能力(思路很重要,思路好的时候,一天可以AC 5道题,要是思路不好,一天可能一道题也做不出来),需要一颗具备把逻辑问题转化为特征方程能力的大脑,总之,做DP问题,是真正用“脑子”做题,不是像搜索一样,靠“模板”来嵌套。
动态规划,难在规划上。对于专题内的题目来说,迷惑性趋向于0。但在专题外,就像老师说的,看完题目,很难向动态规划上去想。所以会觉得很难很难,但一旦想到可能是动态规划的问题,那么基本上答案就呼之欲出了。动态规划是难,难在题干,难在迷惑性强。动态规划又很简单,单一的抽象,单一的方法,单一的循环等等,所以可以这么说:如果一道迷惑性很强的动态规划题,你恰好想到了可能是动态规划并动手去试,那么其实你已经把这题解决了。动态规划拥有很固定的套路,只要按照套路,然后再根据题意修改一下细节,完全是没有难度的。
动态规划在现实中是无处不在的,否则题目也不会有那么强的迷惑性。在现实中解决问题时候,一旦感觉无从下手,试试动态规划的策略模式,往往可能一击中敌,得到答案。
动态规划问题一般的递推关系式:
F[a][b]=max(F[a-1][b],F[a][b-1])+Coin[a][b]
(此步即为递归定义最优解的值,列出状态转移方程)
动态规划问题的一般解题步骤
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
最长上升子序列例题的示例代码如下:
$include<iostream>
Uisngnamespace std;
intb[MAX_N + 10];
intaMaxLen[MAX_N + 10];
intmain()
{
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;
}
intnMax = -1;
for( i = 1;i <= N;i ++ )
if( nMax < aMaxLen[i])
nMax = aMaxLen[i];
printf("%d\n", nMax);
return 0;
}
4.图论
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
图论这个专题我感觉不是一般的难,虽然有模版有算法,解题难度还是很大。首先,老师讲的是图中边和点的存储方法,一个是利用二维数组的邻接矩阵的方式,但限制是点不能很多的,边可以稠密的图。另一个是邻接表,标准的应该是用指针式的链表存储,为了方便和运行速度,修改成结构体的数组来存储,运行速度要快,而且更加方便,它的限制就是边不要太多,点数可以很多的图。而后引出了并查集,就是相同的元素放在一个集合里,用数组表示。当然,这里面有合并集合和查找数值操作,都有固定的模版方法。
下面的才是主菜,一共两道。第一道主菜是最小生成树。最小生成树的定义:所有生成树中权值最小的一个边集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]。其中:
dist1 [u]为从源点v到终点u的只经过一条边的最短路径长度,并有dist 1 [u] =Edge[v][u];
dist2 [u]为从源点v最多经过两条边到达终点u的最短路径长度;
dist3 [u]为从源点v出发最多经过不构成负权值回路的三条边到达终点u的最短路径长度;
……
distn-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中;
最后,谢谢费老师一学期的教导,虽然能力有限,但是还是学到了很多东西,无悔学修ACM!
以上是关于ACM总结报告!的主要内容,如果未能解决你的问题,请参考以下文章