您的位置:首页 > 其它

ACM课程总结

2016-07-08 21:19 190 查看
ACM课程学习总结
时间过得真快,转眼间一个学期又过去了,大二生活临近尾声,ACM课也要结束了。现总结如下:
一、知识点整理
(一)STL简单应用
1、栈 (Stack)
stack是一种先进后出(First In LastOut, FILO)的数据结构,它只有一个出口,只能操作最顶端元素。

2、队列(queue)

queue是一种先进先出(First In FirstOut, FIFO)的数据结构,从底端加入元素,从顶端取出元素。

3、vector-动态数组

头文件: #include <vector>

定义:vector <data_type> vector_name;

    如:vector <int> v;

操作:

    empty() -- 返回bool型,表示vector是否为空(v.empty() )

    size() -- 返回vector内元素个数 (v.size())

    push_back(data_type a) 将元素a插入最尾端

    pop_back() 将最尾端元素删除

    v[i] 类似数组取第i个位置的元素(v[0] )

4、sort函数

头文件: #include <algorithm>

操作:

sort(begin, end);

sort(begin, end, cmp);

例:

   int num[] = {1,5,6,2,9};

    1) sort(num, num + 5);//默认从小到大排序num[] ={1,2,5,6,9};

    2) bool cmp(int a, int b){

       return a > b;

    }

    sort(num, num + 5, cmp); //num[] ={9,6,5,2,1};

5、生成排列

头文件: #include <algorithm>

操作:

bool next_permutation(begin,end);

改变区间内元素的顺序,产生下一个排列。

bool prev_permutation(begin,end);

产生前一个排列。

end为最后一个元素的下一个位置。

6、upper_bound和 lower_bound

upper_bound(begin, end,value);

返回可插入值为value的元素的第一个位置。

lower_bound(begin, end, value);

返回可插入值为value的元素的最后一个位置。

7、set 和multiset

操作:

s.insert(elem) -- 安插一个elem副本,返回新元素位置。

s.erase(elem) -- 移除与elem元素相等的所有元素,返回被移除 的元素个数。

s.erase(pos) -- 移除迭代器pos所指位置上的元素,无返回值。

s.clear() -- 移除全部元素,将整个容器清空。

s.size() -- 返回容器大小。

s.empty() -- 返回容器是否为空。

s.count(elem) -- 返回元素值为elem的元素的个数。

s.lower_bound(elem) -- 返回elem的第一个可安插的位置。  也就是“元素值>= elem的第一个元素位置”。

s.upper_bound(elem) -- 返回elem的最后一个可安插的位置。也就是“元素值> elem的第一个元素的位置”。

以上位置均为一个迭代器。

s.begin() -- 返回一个双向迭代器,指向第一个元素。

s.end() -- 返回一个双向迭代器,指向最后一个元素的下一   个位置

8、map和multimap

所有元素都会根据元素的键值自动排序,map的所有元素都是pair,pair的第一个元素被视为键值,第二个元素为实值。map不允许两个元素有相同的键值,但multimap可以。

头文件: #include <map>

定义:map <data_type1, data_type2>map_name;

     如:map <string,int> m;//默认按string由小到大排序

操作:

m.begin() 返回一个双向迭代器,指向第一个元素。

m.end() 返回一个双向迭代器,指向最后一个元素的下一个    位置。

m.clear() 讲整个容器清空。

m.erase(elem) 移除键值为elem的所有元素,返回个数,对   于map来说非0即1。

m.erase(pos) 移除迭代器pos所指位置上的元素。

直接元素存取:

    m[key] = value;

    查找的时候如果没有键值为key的元素,则安插一个键值为key的新元素,实值为默认(一般0)。

m.insert(elem) 插入一个元素elem

9、优先队列(priority_queue)

一个拥有权值观念的queue,自动依照元素的权值排列,权值最高排在前面。缺省情况下,priority_queue是利用一个max_heap完成的

头文件: #include <queue>

定义:priority_queue <data_type>priority_queue_name;

      如:priority_queue<int> q;//默认是大顶堆

操作:

    q.push(elem) 将元素elem置入优先队列

    q.top() 返回优先队列的下一个元素

    q.pop() 移除一个元素

    q.size() 返回队列中元素的个数

    q.empty() 返回优先队列是否为空

(二)贪心算法
1、概念

在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。

从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。

如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。

2、理论基础

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。

贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。

这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。

利用贪心策略解题,需要解决两个问题:

(1)该题是否适合于用贪心策略求解;

(2)如何选择贪心标准,以得到问题的最优/较优解。

3、贪心算法的求解过程

使用贪心算法求解问题应该考虑如下几个方面:

(1)候选集合A:为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。

(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。

(3)解决函数solution:检查解集合S是否构成问题的完整解。

(4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。

(5)可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。

(三)搜索

1、概念

搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。

相比于单纯的枚举算法有了一定的方向性和目标性。算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态)。

2、状态和状态转移

状态(state)是对问题在某一时刻进展情况的数学描述,或者是数学抽象。

每一个会是答案的一个“可能的”解。状态的转移就是问题从一个状态转移到另一个状态,这样就可以进行搜索的一步步延伸,最后要得到的解也是其中的一个状态。

3、广度优先搜索(BFS)

(1)基本思想:

从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。

生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。

广度优先即是要按层数一层一层来遍历,先将一层全部扩展,然后再进行下一层。

这样利用队列先进先出(FIFO)的性质恰好可以来完成这个任务

[b](2)具体过程:


1.每次取出队列首元素(初始状态),进行拓展

2.然后把拓展所得到的可行状态都放到队列里面

3.将初始状态删除

4.一直进行以上三步直到队列为空。

(3)广度优先搜索框架

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

4、深度优先搜索(DFS)

(1)基本思想:

从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。

DFS的过程,即必须在遍历完所有它的子状态之后,才能继续进行对同一层中下一个状态的遍历。

符合栈的先进后出(FILO)的性质

(2)具体实现过程

1.每次取出栈顶元素,对其进行拓展。

2.若栈顶元素无法继续拓展,则将其从栈中弹出。继续1过程。

3.不断重复直到获得目标状态(取得可行解)或栈为空(无解)。

(3)深度优先搜索框架

递归实现:

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()

Else If 状态Next合法 Then

Stack.push(Next)

End

(四)动态规划

1、动态规划的几个概念

动态规划是解决多阶段决策问题的一种方法。它际上就是一种排除重复计算的算法,更具体的说,动态规划就是用空间换取时间。

多阶段决策问题:

如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.

最优性原理:

不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。

最优决策序列的子序列,一定是局部最优决策子序列。

包含有非局部最优的决策子序列,一定不是最优决策序列。

动态规划的指导思想:

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

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

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

动态规划的几个概念:

阶段:据空间顺序或时间顺序对问题的求解划分阶段。

状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。

决策:根据题意要求,对每个阶段所做出的某种选择性操作。

状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。

2、动态规划问题的一般解题步骤

(1)判断问题是否具有最优子结构性质,若不具备则不能用动态规划。

(2)把问题分成若干个子问题(分阶段)。

(3)建立状态转移方程(递推公式)。

(4)找出边界条件。

(5)将已知边界值带入方程。

(6)递推求解。

3、经典问题-背包问题

1、01背包

特点:每种物品仅有一件,可以选择放或不放,求最大价值

状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]};

伪代码:(空间优化)

fori=1..N

forv=v..0

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

(若要求恰好装满背包,初始化的时候除了将f0]=0,其余的均为负无穷;

若没有要求必须装满,初始化的时候将f[0...v]都设成0)

2、完全背包

特点:每种物品有N件,可以用无限次,求费用总和不超过容量的最大价值

状态转移方程:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i](0<=k<=v/c[j]);

伪代码:(空间优化)

fori..N

forv=0..V

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

(简单优化:将费用大于容量的去掉,两个物品相比,若一个的费用高并且重量小则直接去掉)

3、多重背包

特点:N种物品和容量为V的背包,第i种物品最多有n[i]件可用,求费用总和不超过容量的最大价值

二进制思想:多重背包中的一个物品变成0-1背包中的多个物品

4、分组背包

特点:N件物品容量为V,这些物品被分若干组,每组中的物品发生冲突时最多选一件。转化为每组物品有若干种策略

状态转移方程:f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i](物品i属于k组)

伪代码

fork=1..K(所有k组)

forv=V..0

for所有的i属于k组

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

5、区间DP

区间动态一般都是将一个区间问题不断划分为更小的区间甚至一个元素组成的区间,枚举他们的组合,求合并后的最优值。

区间动态规划的模板代码:

for(intp=1;p<=n;p++){

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

intj=i+p-1;

for(int k=i;k<j;k++)

dp[i][j]=min{dp[i][k]+dp[k+1][j]+w[i][j]};

dp[i][j]=max{dp[i][k]+dp[k+1][j]+w[i][j];

}}

(五)图算法

1、图的基本概念

图:二元组<V,E> 称为图(graph)。 V为结点(node)点(vertex)集。 E为图中结点之间的边的集合。

环:端点重合为一点的边。

重边:两条边连接同一对顶点。

简单图:没有环和重边的图。

无向图:如果边都是双向的,则这个图叫做无向图。

有向图:如果边都是单向的,则这个图叫做有向图。

连通性:

无向图中,如果两个顶点之间存在一条路经,就称这两个顶点是连通的。

有向图中,如果两个顶点之间相互都存在一条路,则称它们是强连通的。

如果一个图的任意两个顶点都是连通的,就称这个图是连通的。

顶点的度:

无向图中,一个顶点相连的边数称为该顶点的度。

有向图中,从一个顶点出发的边数称为该顶点得出度;到达该顶点的边数称为它的入度。

顶点的最大度数称为图的度数。

路和回路:

一个连接两个顶点的,顶点与边交替的序列称为路。

除了起始与终止顶点,其他顶点都不相同,这样的路称为简单路径。起始与终止顶点相同的简单路径称为圈。

完全图、稠密图和稀疏图:

任何两个顶点之间都有边(弧)相连称为完全图。

边(弧)很少的图称为稀疏图,反之为稠密图。

2、图的表示方法-邻接矩阵(Adjacency Matrix)

设图G=(V,E)是一个有N个顶点的图,图的邻接矩阵是一个二维数组G.edge

定义:

If(i,j) ∈E(G),G.edge[i][j]= 1  

Else,G.edge[i][j]= 1

3、图的存储表示-邻接表(Adjacency List)

同一个顶点发出的边链接在同一个边链表中,每一个链结点代表一条边,结点中另一个顶点的下表dest和指针link

邻接表表示法常用于稀疏图

4、最小生成树问题

生成树:由G的n-1条边构成的无环的子图,这些边的集合成为生成树。

最小生成树:所有生成树中权值最小的一个边集T为最小生成树,确定树T的问题成为最小生成树问题。

(1)prim算法

基本思想:

任取一个顶点加入生成树;

在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。

重复上一步骤,直到所有的顶点都进入了生成树为止。

算法:

(1)任意选定一点s,设集合S={s}

(2) 从不在集合S的点中选出一个点j使得其与S内的某点i的距离最短,则(i,j)就是生成树上的一条边,同时将j点加入S

(3)转到(2)继续进行,直至所有点都己加入S集合。

(2)kruskal算法

基本思想:

对所有边从小到大排序;

依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;

直到生成树中有了n-1条边,即告终止。

算法的时间复杂度O(eloge)

算法:

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

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

(3)并查集

一维数组,将所有边按从小到大的顺序存在数组里面

先把每一个对象看作是一个单元素集合,然后按一定顺序将相关联的元素所在的集合合并。能够完成这种功能的集合就是并查集。

对于并查集来说,每个集合用一棵树表示。

它支持以下操作:

Union(Root1, Root2) //合并两个集合;

Findset(x)//搜索操作(搜索编号为x所在树的根)。

树的每一个结点有一个指向其父结点的指针。

5、最短路问题

(1)单源最短路径

bellman-ford算法:

代码:

Bellman-Ford(G,w,s)

     Init(G,s)

     For i 1 to |V[G]|-1

          Do For 每条边(u,v) E[G]

                Do  Relax(u,v,w)

     For每条边(u,v) E[G]

          Do If d[v] > d[u] + w(u, v)

                Then Return FALSE

ReturnTRUE

//时间复杂度为O(V*E);

Bellman-Ford算法能在一般的情况(存在负边权的情况)下解决单源最短路径问题,对于给定的带权有向图G=(V,E),其源点为s,对该图运行Bellman-Ford算法后可以返回一个bool值表明图中是否存在着一个从源点可达的权为负的回路,若存在这样的回路的话,算法说明该问题无解,若不存在这样的回路,算法将产生最短路径及其权值。

spfa算法:

SPFA在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的(不含负圈,较稀疏)情况,k在2左右。算法复杂度理论上同Bellman-Ford,O(nm),但实际上却是O(km)。

Dijkstra算法:

Dijkstra算法解决了有向图G=(V,E)上带权的单源最短路径问题,但要求所有边的权值非负。Djikstra算法中设置了一顶点集合S,从源点s到集合中的顶点的最终最短路径的权值均已确定,算法反复选择具有最短路径估计的顶点u∈V-S,并将u加入S中,对u的所有出边进行松弛。

(2)每对顶点的最短路径

floyd-washall算法

基本思想:

递推产生矩阵序列f(0), f(1), …, f(n)。

其中f(0)[i,j]=map[i,j]

f(k)[i,j]的值表示从vi到vj,中间结点编号不超过k的最短路径长度。

f(n)[i,j]的值就是从vi到vj的最短路径长度。

二、感想
真快,一个学期就这样过去了。选课的时候,大部分同学都退了这门课,纠结了好久,最终还是选上了这门课,想着可以学点东西。开始上课,费老就说了上这门课的要求。当时真的有种后悔的感觉,平时会有那么多作业,糊弄是绝对过不了这门课的。但是后悔一闪而过,更多的是想,这下可以学到知识了,不想学也得学了。像我这样学习主动性和积极性不高的人,就适合这样的方式,这样才能学到东西。结果也确实如此,学到了很多东西。
首先,在知识上,像贪心算法、各种搜索算法、图的算法,数据结构上也提到了这些,但是通过做ACM这些题,写出代码,更让我加深了对这一部分知识的理解。其次,在思想逻辑上,如果不学这门课的话,我不会了解到那种编程思想、那种思维模式,我认为思想、思维层次的学习比知识的学习更为重要。另外,学习这门课充实了我的生活,费老教学风格依旧是那么严谨,平时有时间就需要做ACM题,做出题的成就感和绞尽脑汁想代码哪出错了的那种心急都一样给人带来满足感;通过读英文题也让我认识了不少新单词,一举两得。
总之,在这个学期的课程学习中真的收获了很多。选这门课,我不后悔。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: