ACM课程总结
ACM的课程学习之路自昨天起结束了,本该听他们删掉的选修课程,还是一意孤行地坚持了下来,接触ACM三个月零15天,在HDU做了五六十道题,其实做了多少题目并不是最重要的,重要的是学到了什么,首先简单的总结一下。学到的算法小结:掌握的非常熟悉的算法: 简单的动态规划:包括思考问题的方式和角度,能从简单的问题中提取出动态规划的状态转移方程,关于矩阵内部运算的DP问题比较熟悉,对常见的动态规划问题和经典问题也有一定的了解。 比较简单的贪心:贪心与其说是一种算法,更是一种思想。在构造最优解的每一步过程中,都在构造局部最优解,解这类问题关键是找到最优策略,并分析最优子结构能否构造全局最优解。生成函数问题:稍微想几秒钟能马上把模板给写出来。 大数高精度问题,没什么技术含量,主要是细心,耐心。主要是在具体问题中要区分出哪些问题是大数问题。还有就是代码的简化。 PRIME算法求最小生成树,闭着眼睛可以写出来,其实也是贪心思想的应用吧。 求素数时用的筛法,能简化代码,并熟练运用。 打表方法。欧几里德算法。 调用sort排序。 组合数学上的catalan数的思想。比较熟练的算法: 最短路的DIJKSTRA。 欧几里德算法的推广,用于解不定方程。 二分图最大匹配的匈牙利方法。最大独立集,最小路径覆盖等二分匹配的变种问题。 关系论图论中的拓扑排序。 二叉树的应用:判定树。 对数log的思想解决数论中的一些问题。一些排序算法。 深度优先搜索的思想。 回朔搜索的思想。 二分与分治思想。 搜索中的剪枝技巧。 递规方法以及思想。 运用离散数学中的一些数理逻辑、简单图论、简单数论解决一些问题。组合数学的一些思想方法:fabonacci数列问题、递推方法、排列组合算法的运用。 博弈论简单问题的分析:巴什博弈,尼姆博弈,巴什博弈。判断线段相交的跨立实验。 有些了解的算法: STL的使用:自己学过一点,但是用的不多。 最短路的floyed算法。最小生成树的kruscal算法。并查集的思想。线段树求解RMP问题。以上就是这100多天里学到的知识,以下是各个模块方法的具体总结。
一.贪心算法
一,贪心算法的一般思路是:
1:建立数学模型来描述问题
2:把求解的问题分成若干个子问题。
3:对每一子问题求解,得到子问题的局部最优解。
4:把子问题的解局部最优解合成原来解问题的一个解
二,贪心算法一般用于以下几种问题:
转化为线段覆盖,背包划分,活动时间的安排,数字组合问题
但是该算法也存在一些问题:
不能保证求得的最后解释最佳的;不能用来求最大或着最小解的问题;只能求满足某些约束条件的可行解的范围。
贪心算法一般在开始策略前会进行排序,排序后再进行最优化选择。排序一般直接使用sort函数,在学这门课之前,我写排序还是用最基本版的冒泡或者简单选择。而又学了数据结构,排序的方法就很多了。但是一般我还是会用这个排序,毕竟简单嘛。还有一种很好用的容器是优先队列,这个直接放进数去就可以自动排序,更好用。
图的应用中最小生成树的算法都是很好的贪心算法。事实上贪心算法可以和其他很多算法结合起来,更好用,结果也更准确。
二.搜索算法
搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。
一:二分法 就是折半查找 注意控制精度 还有三分法跟二分差不多
二:DFS深度优先搜索:
它的基本思想是:为了求得问题的解,先选择某一种可能情况向前(子结点)探索,在探索过程中,一旦发现原来的选择不符合要求,就回溯至父亲结点重新选择另一结点,继续向前探索,如此反复进行,直至求得最优解。深度优先搜索的实现方式可以采用递归或者栈来实现。通俗一点就是优先竖着搜索搜索过后标记 直至搜索到要求的情况或者进行到所有结点都被访问。跟前序遍历差不多
把通常问题转化为树的问题是至关重要的一步,完成了树的转换基本完成了问题求解
优化思想:减少所遍历的状态总数。方法有减少节点数,定制回溯边界,记忆化搜索使一些遍历过的子树不再重复遍历。
深度优先搜索的三个原则:
正确性:剪去的“枝条”不包含最优答案;
准确性:在保证第一条原则的情况下,尽可能的剪去更多不包含最优答案的枝条;
高效性:通过剪枝要能够更快的接近到达最优解。
三:BFS广度优先搜索:
其过程为:首先访问初始点Vi,并将其标记为已访问过,接着访问Vi的所有未被访问过可到达的邻接点Vi1、Vi2……Vit,并均标记为已访问过,然后再按照Vi1、Vi2……Vit的次序,访问每一个顶点的所有未被访问过的邻接点,并均标记为已访问过,依此类推,直到图中所有和初始点Vi有路径相通的顶点都被访问过为止。优先横着搜索,一行一行的搜索直到搜索到需要的情况或者结点都被访问。 对于状态数很多时,广度优先搜索可以采用循环队列或动态链表来处理。类似于层序遍历。
四:需要注意的问题
首先是在写程序时遇到运算符,应该考虑的问题:
(1) 一旦做乘法和加法(尤其乘法),就要考虑到会不会有整数溢出的现象,要用32位整数还是64位还是大整数.
(2)一旦在有取模的运算中,做减法一定要考虑会不会出现负数,负数取模在不同的语言中结果是不一样的,所以最好的做法是加特判。
(3)一旦有除法的运算,一定要考虑除数是否为0,有的话就要写特判。
做题注意事项:
(1)有些题目其实是找规律的题目,不要被题目复杂的条件,和很大的数据范围(往往也说明了这些题是有规律的)吓到。可以先从小规模样例入手,还可以先解决去掉一个限制条件的问题,通过这个思路再解决原问题。
(2)注意转化思想,不能太古板,二分搜索不只是查找,还可用于最优解问题。假设结果为x(这样就多了一个条件),然后二分这个x,这样就把最优化问题转化为了判定问题。(有些问题做优化可能不知道如何下手,但转成判定问题就很简单了)
(3)注意把握要领,搜索算法和动态规划算法,都是在多种策略中选取最优解,而贪心算法则不同,它遵循某种规则,不断的选取当前最优策略。
(4)要有好的解题方法,对数据合理的组织(排序,堆),是优化时间的常用方法,也是我们设计数据结构最重要的因素之一,因为多次查找的需要,我们才对一个字典进行排序,这样每次查起来就快很多。如果只用查一次,那我就没有必要对字典排序了。
三.动态规划
动态规划是解决多阶段决策问题的一种方法。
一,多阶段决策问题:
如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策,从而确定了一个过程的活动路线,则称它为多阶段决策问题。
多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.
二,最优性原理
不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。
最优决策序列的子序列,一定是局部最优决策子序列。
包含有非局部最优的决策子序列,一定不是最优决策序列。
三,动态规划的指导思想
在做每一步决策时,列出各种可能的局部解
依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。
以每一步都是最优的来保证全局是最优的。
四,动态规划问题具有以下基本特征:
问题具有多阶段决策的特征。
每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”。
每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态。
每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。
五,动态规划问题的一般解题步骤:
1:判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2:把问题分成若干个子问题(分阶段)。
3:建立状态转移方程(递推公式)。
4:找出边界条件。
5:将已知边界值带入方程。
6:递推求解。
常见的问题有
0-1背包问题,最大子段和,最长单调子序列,数字三角形等。
六,动态规划的编写过程中大家一定注意以下几个问题:
1:边界的处理~数组尽量开大点,防止不小心越界。
2:初始化,一定不要小看初始化,漏一点都会要命的。
3:注意代码编写时变量的准确性,是i还是j、是k还是k-1,一定要看清楚、写清楚,多回过头来检查,防止不该出现的低级失误。
4:动态规划中对于状态的定义一般有多种定义方法,都能解出问题的答案。但是如果状态的定义不同,其时间空间效率也往往有较大的差别。大家在解动态规划问题时一定要尽量多想几种状态定义的方法,然后取其中时空效率最高的一种定义方法进行程序的编写。
5:动态规划有很多时候不能直接用于求解,在大型比赛中往往需要先对问题进行一些必要的处理转化(如排序,最短路径处理)操作才能在此基础上应用动态规划求出问题的最优解。
6:部分问题的思路比较巧妙,需要进行一些猜想和证明才能将其化繁为简,顺利解出。感觉这是动归最难的地方,但也是最常考的。
四.图的相关算法
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
图论,是ACM程序设计这门课的最后一个专题,我觉得也是最难的一个专题,上述所提及的算法,只是图论中算法的极小一部分,图论的综合性比较强,比如有的图论题甚至会用到贪心或搜索的思想,所以遇到具体的问题应该具体的对待。
图论题的另一个特点是模板性非常强,对于常用的算法,只要将其编写成函数,遇到同类问题,只要将现成的函数复制进去,并且修改一下mian()函数和输入输出格式,就可以正确的得出结论。
这里主要就是学了并查集和最小生成树和最短路,最小生成树包括prim算法和kruskal算法最短路常用Dijkstra算法和Bellman-Ford算法
并查集
一,常见两种操作:
合并两个集合
查找某元素属于哪个集合
思想:每次查找的时候,如果路径较长,则修改信息,以便下次查找的时候速度更快
步骤:
第一步,找到根结点
第二步,修改查找路径上的所有节点,将它们都指向根结点
(1)Prim算法:
基本思想:
任取一个顶点加入生成树;
在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。
重复上一步骤,直到所有的顶点都进入了生成树为止。
Prim算法一般适用于稠密图。因为prim算法是针对于结点的算法,边的多少与算法的代价关系不太大。
(2)kruskal算法
kruskal算法的基本思想:
对所有边从小到大排序;
依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;
直到生成树中有了n-1条边,即告终止。
算法的时间复杂度O(eloge)
将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。
最终得到的结果就是最小生成树。
一般和并查集一块使用。
Kruskal算法一般用于稀疏图,kruskal是针对于边的算法,所以边越少越好。
并查集和最小生成树一般是用于将图连通,求最小代价了或者求最少需要修几条路。这一部分就在那一直修路。
(3)Dijkstra算法
把顶点集合V分成两组:
(1)S:已求出的顶点的集合(初始时只含有源点V0)
(2)V-S=T:尚未确定的顶点集合
将T中顶点按递增的次序加入到S中,保证:
(1)从源点V0到S中其他各顶点的长度都不大于从V0到T中任何顶点的最短路径长度
(2)每个顶点对应一个距离值 S中顶点:从V0到此顶点的长度 T中顶点:从V0到此顶点的只包括S中顶点作中间顶点的最短路径长度
依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和
求最短路径步骤
算法步骤如下:
G={V,E}
1. 初始时令 S={V0},T=V-S={其余顶点},T中顶点对应的距离值 若存在
2. 从T中选取一个与S中顶点有关联边且权值最小的顶点W,加入到S中
3. 对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值 重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止
(4)Bellman-Ford算法:
Bellman-Ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。其原理为持续地进行松弛(原文是这么写的,为什么要叫松弛,争议很大),在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。Bellman- ford算法有一个小优化:每次松弛先设一个标识flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间去做没有必要的松弛,而SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。
算法描述:
1,.初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0;
2.迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次)
3.检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。;
注意Dijkstra算法不能处理带有负权值的情况,此时需要用Bellman-Ford算法;
匆匆茫茫的开始,又匆匆茫茫的结束,在这一百多天确实学到了不少关于算法的知识,相信在以后的编程中对我也将有很大帮助,希望此总结也能帮助你们更好的理解acm算法!