ACM 总结报告

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM 总结报告相关的知识,希望对你有一定的参考价值。

ACM总结

序:

   起初,并不知道ACM是个什么玩意,但是听老师说,感觉很不错,关键是感觉跟着费老做,不会走错。所以就选修了ACM这个课程。

   自己之前没有接触过这个东西,所以对算法这个陌生的词汇实在知之甚少,做题目什么的,完全没有一点知识的东西,说实话 --- 全靠想象。

学ACM已经一个学期了,在这个还不算残酷的学期里,经过了  上课 --- 认真听 —- 周六周日比赛训练 --- 自学 --- 不认真听 --- 参加省赛 --- 认真听加自学。原因是什么, 在做ACM题目的时候,很是感觉上ACM课程里学的东西,和比赛里用到的,相差太大,实在知道的太少。所以就自己看书学知识点,但是自己学了一段时间后发现有老师讲的情况下比自己学效率高多了,所以认真听课才是王道。

说实话学ACM确实难,对数学要求很高,当然英语也不能很差:学ACM确实辛苦,课余时间实际上和没有差不多,一心一意搞ACM,才能搞好。

只要路在脚下,就没有尽头。

 

准备篇  STL

   STL是非常好用的一个东西,不仅仅在ACM用,在做系统开发的时候,依然很好用。对于没有接触STL的,

无疑是“生命不完整,人生有缺憾”。哈哈

   STL中有不定长的vector数组,比正常的数组好用多了,想增加就直接push_back()进去,想删除直接erase()就可以,而且排序也方便了很多,系统自定义的sort格外好使,vector的方便之处主要还是在数组定义的时候不必规定长度,

   Map multimap 说实话用的很少,但是在做系统的时候用到的非常多,查找的时候,关键字关联位置直接找到位置输出,非常快,比传统的遍历快上N倍

   队列 ,优先队列,按照先进先出的原则进行,优先队列按照某种顺序直接排列好的一种队列(甚是好用),

在处理很多问题是都行,最常见的 图论中出现,

   栈 这是个按照先进后出的原则进行,用这个现在用得很少,用这个东西解决过一道多项式运算的题目,

   要想成为一名计算机高手,入门是必须的,入门第一步,也就是我们学习的第一课—STL,《ACM程序设计》中,就介绍了很多STL基础的运用方法,足够我们写算法,实战之中方便快捷,实在是所有学习算法的入门必选,经过了整个学期的学习,受益匪浅,但是没想到还有更难得关卡在等着我们。加油!!

   对于STL这个东西,不学习它,真的是“生命不完整,人生有缺憾”。

  

第一章  贪心算法

   说到贪心算法,应该是算法里最简单的了,这也是老师为什么开始先讲这个的原因,

 

贪心算法就跟名字一样解决问题一定要“贪”,拿田忌赛马这个例题来讲,他和大王赛马的原则就是不能吃亏,能赢的就要赢,赢不了就要用自己最次的马和你最好的马跑,浪费掉你最好的马。

贪心的一般过程就是:

(1) 将所给数据按照一定的顺序进行排序;

(2) 从头开始进行处理问题;

 

   对待贪心问题都要以“贪”为主,这类问题容易与DP混淆,一定要抓住贪心问题的特点以及贪心问题的解决思路,关键还是要确定用贪心这个算法能解决问题。

   虽然简单,但是一定不要大意。。。

 

第二章    搜索算法

  一入搜索深似海,不知何年是归期,^0^。搜索就是枚举中的一颗明珠,搜索和枚举其实是一样的,只是处理了一下,搜索对图的题目很好解决,但是用枚举就很难去做了。

这是一个心酸的故事,你可能不信,一开始我是拒绝的,没有什么问题是搜索解决不了的,如果有那就写两个搜索。学搜索开始的日子总是难熬的,广搜还好点,深搜就费劲了,递归没学明白,这个地方更是稀里糊涂的,看了一遍遍代码还是不明白,只能一遍遍的走程序,真是煎熬,深搜状态多,就拿联通块的问题来说,每一步都可能搜出来四步,很难弄的,光写就会写很多,好容易的把这个弄明白了,但是又迎来了又一大难题——超时!!!!!得想尽办法来剪枝,好烦啊!!!

  1、二分 + 三分

二分三分:

 

二分可用于具有单调性的直线求零点,通过中间值与两段的值的比较不断地缩小范围就可以在一定的精度范围内求出解。

一般的解体模板为:

While(abs(left-right) > 1e-6)

{

   Mid=(left-right)/2;

If(F(mid)>0)

    Left=mid;

Else

     Right=mid;

}

 

而对于三分,一般的方程的图线为 二元一次方程的图,取中,再取中点与头或尾的中,来缩小范围,

一般模板为:

while(fabs(left-right)>1e-6)//三分来判断最大的那个角度

{

   Mid = (left-right)/2;

   midmid =(mid + left)/2;

   if(F(mid)>F(midmid))

      right=midmid;

   else

      left=mid;

}

当然有时候一道题可能有两种方法结合求解。

 

2、广搜

                          

先将树顶的元素压入栈中,将他所有的儿子都搜出来压入栈中,这个搜索的元素就没有价值了,就可以将它出栈,接着只要栈不空就继续刚才的操作,直到栈空了,或者你找到你想要的解了。

一般的方法解决:

通常用队列(先进先出,FIFO)实现

Q={起点s}; 标记s为己访问;

while (Q非空)

 {

取Q队首元素u; u出队;

所有与u相邻且未被访问的点进入队列;

标记u为已访问;

   }

由BFS得到的路径是原图中从S到T的边数最少的路径

广度优先搜索树不唯一

 

3、深搜

  深度优先搜索,比起广搜,深搜就搜索的更加的多,在求解路径的时候,没有目的地的时候深搜更加的好用,但是当有目的地的时候。用广搜又快又准。

当然深搜也有相应的模板:

 

int timestamp = 0;

dfs(int p) {

   timestamp = timestamp + 1;

   col[p] = GREY;

   d[p] = timestamp;

   for (每个与p相邻的点i)

      if (col[i] == WHITE) dfs(i);

   timestamp = timestamp + 1;

   f[p] = timestamp;

   col[p] = BLACK;

}

最明显的便是用递归实现了搜索。

3、记忆化搜索

记忆化搜索 和 DP有些类似,用数组记录下来搜索的状态,每次搜索得到的结果和上一个状态进行比较取最优的结果,唯一有点不同的就是,DP一般是从后往前更新状态,但是记忆化搜索是从前往后更新状态。

记忆化搜索的优点在于,对于有些重复的计算,只计算一次再存起来,第二次遇到的相同计算的时候直接调用之前结果,这样可以大大节约时间,在很多题目中记忆化搜索这种思想是解决超时问题的一个关键。

 

小结:

   对于搜素,虽然讲了这么多,但是还有很多地方还没有扩展,

搜索与图论是分不开的,可以说解决图论问题的一种方法就是搜索。

 

第三章   动态规划

   关于动态规划,说实话这一章真的很难,有时候想到这个算法,也实现不了,还要去找到状态转移方程。真是难为人呀!

关于动态规划的只是还真不少,这是一种 多阶段决策问题

 

最优性原理

在做每一步决策时,列出各种可能的局部解

依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。

以每一步都是最优的来保证全局是最优的。

看起来像贪心思想,但是并不是那么简单。

做动态规划的时候,自己感觉是从后向前想问题,从结果出发,到达最初。

最基础的就是 最长上升子序列,最大字段和问题,最大子矩阵和问题 等,很多问题都可以转化为这些简单问题。

当然经典的动态规划背包问题才是难点:

01背包:

 

把这个过程理解下:在前i件物品放进容量v的背包时,

它有两种情况:

 

For(int i=1;i<=n;i++)

For(int j=v;j>=c[i];j--)

F[i][j]=max(F[i-1][v],F[i-1][j-c[i]]+w[i]);

第一种是第i件不放进去,这时所得价值为:f[i-1][v]

第二种是第i件放进去,这时所得价值为:f[i-1][v-c[i]]+w[i]

 

这个图清晰明了,每次更新状态都是i行和i-1行作比较,谁的状态更优就取哪一个,最优解就是最后一个状态f[n][v];

 

还有:可以空间优化 方法

for i=1..N

    for v=V..0

        f[v]=max{f[v],f[v-c[i]]+w[i]}; 

初始化细节:

若要求恰好装满背包,初始化时除了f[0]为0其它f[1..V]均设为-∞

若没有要求必须把背包装满,初始化时将f[0..V]全部设为0

 

完全背包:

伪代码:

for i=1..N

    for v=c[i]..V

        f[v]=max{f[v],f[v-c[i]]+w[i]}

for(int j=c[i];j<=v;j++)

max();

j=c[i]+1

代码:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

解析:这里的max中的两项就是当前状态的值了,为何?

因为每种背包都是无限的。当我们把i从1到N循环时,f[v]表示容量为v在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我们要考虑的当然是当前状态。

转化为01背包:

   考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题

伪码: 

for i=1..N

   for v=0..V

      f[v]=max{f[v],f[v-cost]+weight}

   注意:此处v的循环次序和01背包的循环次序不同!!!

      假如合理的交换v,n的循环次序,貌似可以稍稍的提高效率.

 

多重背包:

 

多重背包可以这么理解遍历到当前物品时如果物品的总体积小于背包的剩余体积那么就可以看成是一个01背包问题,如果大于的话就可以看成是一个完全背包问题;这样多重背包问题就能解决了。

完全背包还有一个插曲:

二进制优化,

这样可以大大的节省时间,因为任何数都能由几个二进制数和一个非二进制数组成,比如9=2*2*2+1;这样就能将完全背包转化为01背包,将二进制数看成物品,取还是不取,找到最有的解决方案

 

第四章  图论

1、并查集

并查集就是合并查找,最短路径;

(1)“建树”:通俗的建树,就是将所有联通的点用一个标记标记出来,就形成了一个“数”,例如在1-10这十个点中,1 3联通那么1 3的就可以用相对小的1来标记。

(2)查找:想查找两个点是不是联通的时候,只需要找到两个点的根是不是同一个根,如果是那么就是联通,不是就不是联通。

 

狄克斯特拉算法(dijkstra):

 

 

我们这里定义图的编号为:

1 2 3

4 5 6

7 8 9

图1:初始化的图,其中包含边的权值(耗时)。(这里图是有向图)。

图2:确定起点,然后向能直接走到的点走一下,记录此时的估计值:2 6 9.。

图3:找到距离起点最近的点,是正东边的那个点,这时候我们耗费权值为2。然后我们进行松弛操作,从起点到其东南方的点直接到的权值耗费为6,但是我们通过刚刚选定的点,我们找到了到这个点更近的方式,所以这个时候我们说从起点到其东南方向的点的权值更新值从6变成了5。这个时候我们就完成了第一次松弛操作。

图4:依旧是找距离起点最近的点。然后松弛我们发现这个时候从起点到其东南方的点的耗费权值从5又变成了4.这个时候我们完成了第二个松弛。

之后的方式同上:选定距离起点最近的点v。然后通过点v进行松弛操作。我们发现能够通过增加走到目的地方式的复杂度(多转弯)的方式我们能够松弛掉权值,使得耗费的权值更小。

     模板:

void Dij()//我们这里起点为1号编码点。我们这里的d[]表示从起点到这个点需要的权值。w[a][b]表示点a到点b这条边的权值.

{

 int i,j,k,v,tmp;

 memset(vis,0,sizeof(vis));

 for(i=1;i<=n;i++)

     d[i]=w[1][i];//对应图不难理解,对于起点的初始化

 d[1]=0;

 vis[1]=1;

 for(i=1;i<=n;i++)//控制连接点的次数,例如上图,九个点,就循环九次。

 {

  tmp=N;//这里N表示无穷大。也就是图上的99.

  for(j=1;j<=n;j++)

  {

   if(tmp>d[j]&&!vis[j])

   {

    tmp=d[j];

    v=j;

   }

  }//每次我们都找到距离起点最近的点v

  vis[v]=1;

  for(k=1;k<=n;k++)//然后进行松弛操作。

我们这里的d[]表示从起点到这个点需要的权值//加以强调其含义。

{

  if(!vis[k])

   d[k]=min(d[k],d[v]+w[v][k]);

}}}

-----------------------------------------------------------图的存储方式 — 邻接矩阵

限制:

数据规模不大n <= 1000,m越大越好

稠密图最好用邻接矩阵

图中不能有多重边出现

图的存储方式 — 邻接表

限制:

顶点数很多n>1000,边数却不多。

采用邻接表存储后,很多算法的复杂度也都是跟边数有关。

连通性的问题很多情况边数不多,多采用邻接表存储方式

 

最小生成树

无向图最小生成树,prim算法,邻接阵形式,复杂度O(n^2)

必须保证图的连通的!

 

Kruskal算法:

1、将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。

2、最终得到的结果就是最小生成树。

3、并查集

最短路径

Dijkstra算法:

初始化数组dist、path和s;

 while (s中的元素个数<n)

     1 在dist[n]中求最小值,其下标为k;

     2 输出dist[j]和path[j];

     3 修改数组dist和path;

     4 将顶点vk添加到数组s中;

Dijkstra的缺陷就在于它不能处理负权回路

Bellman-Ford算法:

采用递推方式计算 dist k [u]。

设已经求出 dist k-1 [u] , u = 0, 1, …, n-1,此即从源点v最多经过不构成负权值回路的k-1条边到达终点u的最短路径的长度。

从图的邻接矩阵可以找到各个顶点j到达顶点u的距离Edge[j][u],计算min{ dist k-1 [j] + Edge[j][u] } ,可得从源点v绕过各个顶点,最多经过不构成负权值回路的k条边到达终点u的最短路径的长度。

比较dist k-1 [u]和min{ dist k-1 [j] + Edge[j][u] } ,取较小者作为dist k [u]的值。

Spfa 算法:

SPFA 其实就是Bellman-Ford的一种队列实现,减少了冗余,即松驰的边至少不会以一个d为∞的点为起点。

算法:

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),稀疏图的最短路

 

最后,学习ACM的课程结束了,但是学习算法设计的路还没有结束,只要路在脚下,就没有尽头。

   俞敏洪说过:一个男人,是需要孤独的,如果你忍受不了孤独,只能说明你内心还不够强大,一个真正优秀的男人一定是朴实无华,甚至是谨小慎微的,一定是一个孤独者,因为只有静才能生智,而不是急!

最后以 秋本华 的一句结束:要么孤独,要么庸俗。

以上是关于ACM 总结报告的主要内容,如果未能解决你的问题,请参考以下文章

ACM 总结报告

ACM总结报告

acm课程总结报告

ACM学习报告

知识点总结报告 1.20

知识点总结报告 1.22