标签:
首先谈一下对acm的了解:ACM即acm国际大学生程序设计竞赛,是由国际计算机学会主办的,一项旨在展示大学生创新能力、团队精神和在压力下编写程序、分析和解决问题能力的年度竞赛。经过近30多年的发展,acm国际大学生程序设计竞赛已经发展成为最具影响力的大学生计算机竞赛。
时间过的飞快,让人来不及可惜,就早已悄悄溜走,不知不觉中这个学期也已经接近尾声,这十几个周的acm课程也算是告落一段了,说实在的我挺喜欢这个课的,不过自己似乎并没有拿出充足的时间去钻研它,不过这近一年的学习也算是让我小有收获,虽然还是有好多不会的地方,但我觉得我还是可以继续学习解决的虽然这门课程已经结束了,近期费老师说要开始选拔参赛训练的队伍,我仔细考虑了一下,最终决定还是不去参加了,前面说的学的并不是很好是一方面的原因,还有就是我怕我自己到时候拿不出那么多的时间去做这件事,耗费了时间跟精力却得不到什么收获。其实一开始并没有选修这门课,后来因为我们c++老师的推荐所以就去上了几节然后觉得还不错,也就一直学到了现在,算法这个东西学好了还是很有用的,用算法对代码进行优化后效率提高了很多。
这十六周的学习,仔细想想也没有学太多的东西,就是四个部分,贪心,搜索,动态规划和图的一部分应用,当然我们还没学数据结构。。。。。。虽然看起来东西不多,但掌握起来确实有难度,要是应用到写代码上就更有难度了,像动态规划,搜索之类的,就是那么几步解题过程,如果找不到解题思路,那就惨了,有时候甚至一天都可能做不出一道题来。
说了好多废话下面来谈谈所学的第一个专题,在我去上课的时候这一部分就已经快讲完了,所以大部分都是靠自己看书还有课件学的,但总觉得自己学的不完善,这一部分做的题也不是很多,可能还有很多不足的地方,还望见谅。
一、贪心算法
贪心算法的一般思路是:
建立数学模型来描述问题 把求解的问题分成若干个子问题。 对每一子问题求解,得到子问题的局部最优解。 把子问题的解局部最优解合成原来解问题的一个解
贪心算法一般用于以下几种问题: 转化为线段覆盖 背包划分 活动时间的安排 数字组合问题
但是该算法也存在一些问题:
不能保证求得的最后解释最佳的; 不能用来求最大或着最小解的问题; 只能求满足某些约束条件的可行解的范围。
一般的题型是:
N个商品,每个商品的重量为WI,价格为:PI,现有一个背包,最多能装M的重量.其中(0<=I< N,0< wi< M).
问:怎样装能使包中装入的商品价值最高(对于每个商品可以只装该商品的一部分)
代码:
参数分别为 n:物品数 M:背包最多能装的重量 v[]:价值数组 w[]重量数组?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void beg(intn,double M,double v[],double w[],double x[])
{
Sort(n,v,w);
inti;
for(i =
1; i <= n ; i++)
x[i] =
0;
double c=M;
for(i=1;i<=n;i++)
{
if(w[i] > c)
break;
x[i]=1;
c-=w[i];
}
if(i <= n)
x[i]=c / w[i];
}
|
贪心算法一般在开始策略前会进行预处理,比如一般先排序,排序后再进行最优化选择。排序一般直接使用sort函数,在学这门课之前,我写排序还是用最基本版的冒泡或者简单选择。后来也学了其他的一些方法,但是一般我还是会用这个排序,毕竟简单嘛。还有一种很好用的容器是优先队列,这个直接放进数去就可以自动排序,更好用。图的应用中最小生成树的算法都是很好的贪心算法。事实上贪心算法可以和其他很多算法结合起来,更好用,结果也更准确。
搬桌子的问题,我觉得这个题中从3号搬到7号和从1号搬到5号房间是可以同时搬的,一前一后走就没问题,但是这个题中不能同时走。这也是贪心算法的一种限制吧。当时的解题思路是一号和二号房间占用同一块走廊空地 以此类推 一共有200块走廊空地 如果每一次移动桌子都会在空地上留下一道痕迹 痕迹有重叠的地方是不能同时移动桌子的情况 那么 在这200块空地上 最多的重叠痕迹就是最多的移动时间。每一道重叠痕迹都需要10min钟。
还有田忌赛马问题也是用了很经典的贪心思想,进行排序处理,如果田忌最好的马能赢国王最好的马,让他俩比一局;如果田忌最差的马能赢国王最差的马让他俩比一局;如果上面两个都不行让田忌当前最差的马与国王最好的马比一局。
二、搜索算法
搜索算法分为BFS和DFS。
深度搜索与广度搜索的控制结构和产生系统很相似,唯一的区别在于对扩展节点选取上。由于其保留了所有的前继节点,所以在产生后继节点时可以去掉一部分重复 的节点,从而提高了搜索效率。这两种算法每次都扩展一个节点的所有子节点,而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索 扩展的则是本次扩展的节点的兄弟节点。在具体实现上为了提高效率,所以采用了不同的数据结构.
搜索算法的优化:
一、双向广度搜索
广度搜索虽然可以得到最优解,但是其空间消耗增长太快。但如果从正反两个方向进行广度搜索,理想情况下可以减少二分之一的搜索量,从而提高搜索速度。
二、分支定界
分支定界实际上是A*算法的一种雏形,其对于每个扩展出来的节点给出一个预期值,如果这个预期值不如当前已经搜索出来的结果好的话,则将这个节点(包括其子节点)从解答树中删去,从而达到加快搜索速度的目的。
三、A*算法
A*算法中更一般的引入了一个估价函数f,其定义为f=g+h。其中g为到达当前节点的耗费,而h表示对从当前节点到达目标节点的耗费的估计。其必须满足两个条件:
1. h必须小于等于实际的从当前节点到达目标节点的最小耗费h*。
2. f必须保持单调递增。
A*算法的控制结构与广度搜索的十分类似,只是每次扩展的都是当前待扩展节点中f值最小的一个,如果扩展出来的节点与已扩展的节点重复,则删去这个节点。如果与待扩展节点重复,如果这个节点的估价函数值较小,则用其代替原待扩展节点。
对A*算法的改进--分阶段A*. 当A*算法出现数据溢出时,从待扩展节点中取出若干个估价函数值较小的节点,然后放弃其余的待扩展节点,从而可以使搜索进一步的进行下去。
四、A*算法与回溯的结合(IDA*)
这是A*算法的一个变形,很好综合了A*算法的人工智能性和回溯法对空间的消耗较少的优点,在一些规模很大的搜索问题中会起意想不到的效果。它的具体名称 是 Iterative
Deepening A*, 1985年由Korf提出。该算法的最初目的是为了利用深度搜索的优势解决广度A*的空间问题,其代价是会产生重复搜索。归纳一下,IDA*的基本思路 是:首先将初始状态结点的H值设为阈值maxH,然后进行深度优先搜索,搜索过程中忽略所有H值大于maxH的结点;如果没有找到解,则加大阈值
maxH,再重复上述搜索,直到找到一个解。在保证H值的计算满足A*算法的要求下,可以证明找到的这个解一定是最优解。在程序实现上,IDA*
要比 A* 方便,因为不需要保存结点,不需要判重复,也不需要根据 H值对结点排序,占用空间小。
下面,以一个具体的实例来分析比较上述几种搜索算法的效率等问题。
在scu online judge(http://cs.scu.edu.cn/acm)上有这么一道题目:这就是古老而又经典的15数码难题:在4*4的棋盘上,摆有15个棋
子,每个棋子分别标有1-15的某一个数字。棋盘中有一个空格,空格周围的棋子可以移到空格中。现给出初始状态和目标状态,要求找到一种移动步骤最少的方 法。
看到这个题目,会发觉几乎每个搜索算法都可以解这个问题。而事实确实如此。
首先考虑深度优先搜索,它会遍历这棵解答树。这棵解答树最多可达16!个节点,深度优先搜索必须全部遍历后,才能从所有解中选出最小的一个做为答案,其代价是非常巨大的。
其次考虑广度深度优先搜索,这不失为一个好办法。因为广度优先搜索的层次遍历解答树的特点,一旦搜索到一个目标节点,那么这时的深度一定是最优解,而不必 象深度优先搜索那样继续搜索目标节点,最后比较才能得出最优解。该搜索方法在这道题目上会遇见致命的问题:广度深度优先搜索是一种盲目的搜索,深度比较大
的测试数据会产生大量的无用的节点,同时消耗很多时间在重复节点的判断上。
为了减少重复的节点,加入人工智能性,马上可以想到用A*算法。经过分析发现,该方法对避免产生大量的无用的节点起到了一定的效果,但是会花97%以上的 时间去判断新产生节点是否与已扩展的和待扩展的节点重复。看来如何提高判重的速度成为该题目的关键。解决这个问题有很多办法,比如引入哈希表,对已扩展的
和待扩展的节点采用哈希表存储,减少判重的代价,或者对已扩展的和待扩展的节点采用桶排序,也可以减少判重的代价。我们现在来尝试一下用 IDA*算法。该算法有个值得注意的地方:对估计函数的选取。如果选用当前状态每个位置上与目标状态每个位置上相同节点的数目加当前状态的深度作为估计函 数,由于当前状态每个位置上与目标状态每个位置上相同节点的数目这个值一般较小,不能明显显示各个状态之间的差别,运行过程中会产生大量的无用的节点,同
样会使效率很低,不能在60s以内完成计算。比较优化的一个办法是选用由于当前状态每个位置上的数字偏离目标节点该数字的位置的距离加当前状态的深度作为 估计函数。这个估计函数的选取没有统一的标准,找到合适的该函数并不容易,但是可以大致按照这个原则:在一定范围内加大各个状态启发函数值的差别。
实践证明,该方法用广泛的通用性,在很多情况下可以替换一般的深度优先搜索和广度优先搜索。
二分算法:二分算法主要分为二分查找和三分搜索。
二分查找主要针对的是单调函数给定函数值,求自变量值的情况,非常简单,优点是比较次数少,查找速度快,平均性能好,二分查找的基本思想是将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 时,再比较下这两个点的值,我们就找到了解。
搜索算法与其他算法结合:
该部分将谈到搜索与其他算法的结合。我们看看杭电上做过的一道经典题目: 给定一个8 * 8的国际象棋棋盘。给出棋盘上任意两个位置的坐标,问马最少几步可以从一个位置跳到另外一个位置。
该题目同样是求最优解,如果用一般的深度优先搜索是很容易超时的。如果用广度优先搜索,会消耗大量的内存,而且效率是很低的。这里,我们将尝试用深度优先搜索加动态规划的算法解决该问题。
将该棋盘做为存储状态的矩阵。每个矩阵元素的值是该位置到初始位置最少需要的步数。初始位置的元素值为0。其他位置的元素初始化为一个很大的正整数。首先 从初始位置开始深度优先搜索,例如某次从(i1,j1)到达位置(i2,j2),如果(i2,j2)处的值大于(i1,j1)的值加1,则(i2,j2)
处的值更新为(i1,j1)+ 1,表示从(i1,j1) 跳到(i2,j2)比从其他地方跳到(i2,j2)更优,不断的进行这个过程,直到不能进行下去位置,那么最后的目标位置的值就是解。这就是一个动态规划 的思想,每个位置的最优解都是由其他能够一次跳到这个位置的位置的值决定的,而且是它们中的最小值。同时,该动态规划又借助深度优先搜索这个工具,完成对
每个位置的值的刷新,可以算是一个比较经典的深度优先搜索和动态规划的结合。该问题还需要注意一个剪枝的问题,从起始位置到目标位置的最大步数是多少?经 过计算,最大值是6。所以一旦某个位置的值是6了,就不必再将它去刷新另外的位置,从而剪去了对很多不必要子树的搜索,大大提高了效率。
三、动态规划问题
一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
四、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
六、动态规划算法基本框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
for(j=1; j<=m; j=j+1) //
第一个阶段
xn[j] = 初始值;
for(i=n-1; i>=1; i=i-1)//
其他n-1个阶段
for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式
xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};
t = g(x1[j1:j2]); //
由子问题的最优解求解整个问题的最优解的方案
print(x1[j1]);
for(i=2; i<=n-1; i=i+1)
{
t = t-xi-1[ji];
for(j=1; j>=f(i); j=j+1)
if(t=xi[ji])
break;
}
|
经典问题:背包问题,数塔问题,字段和问题,公共子序列问题等等;
背包问题又分为01背包,其dp方程大多相似像c[i][m]=max{c[i-1][m-w[i]]+pi , c[i-1][m]},还有完全背包,这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出dp方程,f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v},最后还有多重背包,分组背包等问题。
四、图算法
并查集:
将编号分别为1…N的N个对象划分为不相交集合,
在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
合并两个集合
查某元素属于哪个集合
优化:
每次查找的时候,如果路径较长,则修改信息,以便下次查找的时候速度更快
步骤:
第一步,找到根结点
第二步,修改查找路径上的所有节点,将它们都指向根结点
最小生成树:
(1)Prime算法: (1) 集合MST=T=Empty,选取G中一结点u,T.add(u)
(2) 循环|V|-1次:
选取一条这样的边e=min{(x,y) | x in T, y in V/T}
T.add(y); MST.add(e);
(3) MST即为所求
(2) Kruskal算法 (1) 将G中所有的边排序并放入集合H中,初始化集合MST=Empty,初始化不相交集合T={{v1}, {v2}...}},也即T中每个点为一个集合。
(2) 依次取H中的最短边e(u,v),如果Find-Set(u)!=Find-Set(v)(也即u、v是否已经在一棵树中),那么Union(u,v) (即u,v合并为一个集合),MST.add(e);
(3) MST即为所求
这两个算法都是贪心算法,区别在于每次选取边的策略。证明该算法的关键在于一点:如果MST是图G的最小生成树,那么在子图G‘中包含的子生成树MST‘ 也必然是G‘的最小生成树。这个很容易反正,假设不成立,那么G‘有一棵权重和更小的生成树,用它替换掉MST‘,那么对于G我们就找到了比MST更小的生成树,显然这与我们的假设(MST是最小生成树)矛盾了。
理解了这个关键点,算法的正确性就好理解多了。对于Prime,T于V/T两个点集都会各自有一棵生成树,最后要连起来构成一棵大的生成树,那么显然要选两者之间的最短的那条边了。对于Kruskal算法,如果当前选取的边没有引起环路,那么正确性是显然的(对给定点集依次选最小的边构成一棵树当然是最小生成树了),如果导致了环路,那么说明两个点都在该点集里,由于已经构成了树(否则也不可能导致环路)并且一直都是挑尽可能小的,所以肯定是最小生成树。
最短路径:
这里的算法基本是基于动态规划和贪心算法的,经典算法有很多个,主要区别在于:有的是通用的,有的是针对某一类图的,例如,无负环的图,或者无负权边的图等。
单源最短路径(1) 通用(Bellman-Ford算法):
(2) 无负权边的图(Dijkstra算法):
(3) 无环有向图(DAG) :
这个学期收获很多,也很感谢acm,它让我不断成长。
acm课程总结
标签:
原文地址:http://blog.csdn.net/ibingyu/article/details/51787306