ACM课程总结
本学期我选修了ACM课程,一共学习了五个模块,四种算法——首先介绍了STL的简单使用,接下来学习了贪心算法、搜索算法、动态规划算法和图算法。
第一讲:STL
在课程的开始,老师先向我们介绍了标准模板库里边的几种结构,在排序和查找方面都非常有用。
比如说栈:头文件<stake>是一种先进后出的数据结构,主要有以下操作——
1、empty()–返回值是bool类型,表示栈内是否为空;
2、size()–返回值是栈内元素的个数;
3、top()–返回栈顶元素;
4、pop()–移除栈顶元素;
5、push()–向栈压入一个元素。
再如队列:头文件<queue>是一种先进先出的数据结构,主要有以下操作——
1、empty()size()同上;
2、front()–返回queue的下一个元素;
3、back()–返回queue的最后一个元素;
4、pop()–移除queue中的一个元素;
5、push()–将一个元素置入queue中。
再如vector数组:头文件<vector>,主要操作有——
1、empty()size()同上;
2、push_back()将最尾端元素删除;
3、v[i]类似于数组取第 I 个位置的元素。
还有sort:它的头文件是<algorithm>,主要操作有sort(begin,end),sort(begin,end,cmp);
生成排列:头文件<algorithm>.主要操作——
改变区间内元素顺序:bool next_pemutation(begin,end);bool pre_permutation(begin,end);upper_bound(begin,end,value)返回可插入值value 的元素的第一个位置;lower_bound(begin,end,value)返回可插入值value的元素的最后一个位置。
set和multiset;头文件为<set>。会根据特定的排列法则,自动将元素排序,multiset允许有相同元素;默认从小到大排序。它的简单应用有——
1、insert()–安插一个副本,返回新元素位置;
2、eraser(a)–移除与a元素相同的元素,返回一出元素的个数;
3、eraser(pos)–移除迭代器pos所指位置的元素,无返回值;
4、clear()–移除全部元素,清空容器。
5、size()–返回容器大小;
6、empty()–返回容器是否为空;
7、count(a)–返回元素值为a的元素个数;
8、lower_bound(a)–返回a可安插的第一个位置;
9、upper_boound(a)–返回a可安插的最后一个位置;
10、begin()–返回一个双向迭代器,指向第一个元素;
11、end()–返回一个双向迭代器,指向最后一个元素的下一个位置。
map和multimap:头文件<map>。所有元素都会自动排序,map的所有元素都是pair,pair的第一个元素被视为键值,第二个元素称为实值,map不允许两个元素有相同的键值,multimap可以。主要操作和set类似,除此之外还有——
1、运用value_type插入
map<string,float>m;insert(map<string,float>::value_type();
2、运用pair<>
insert(pair<string,float>();
3、运用make_pair()
insert(pair<make_pair();
优先队列:拥有权值的queue,自动依照元素的权值排列,权值高的在前。头文件<queue>,主要操作——
1、push(a)将元素a插入优先队列;
2、top()返回优先队列的第一个元素;
3、pop()移除一个元素;
4、·········
第二讲:贪心算法
什么是贪心算法呢?
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
贪心算法的理论基础:
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。
贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。
这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。
利用贪心策略解题,需要解决两个问题:
(1)该题是否适合于用贪心策略求解;
(2)如何选择贪心标准,以得到问题的最优/较优解。
在使用贪心算法时要考虑一下问题:
1)候选集合A:为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。
(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
(5)可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。
贪心算法的一般流程:
G(a)
{
s={}; //初始化解集合为空
while (not solution(s)) //集合s没有构成问题的解
{
x=select(a); //在候选集合中做贪心算法
if feasible (s,x) //判断集合s中加入x后的解是否可行
s=s+{x};
a=a-{x};
}
}
(1)候选集合A:问题的最终解均取自于候选集合A。
(2)解集合S:解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:贪心策略,这是贪心算法的关键。
(5)可行函数feasible:解集合扩展后是否满足约束条件。
例如活动安排问题,大体题意如下:
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。
每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在半开时间区间[si ,fi )内占用资源。若区间[si ,fi )与区间[sj,fj )不相交,则称活动i与活动j是相容的。当 si ≥ fj 或 sj ≥ fi 时,活动i与活动j相容。
活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。
根据题意有以下分析:
数据结构:struct action{
int s,t,index;//起止时间和活动编号
};
活动的集合记为数组: action a[1000];
按活动的结束时间升序排序;
排序比较因子:
bool cmp(const action &a,const action&b)
{
if(a.f<=b.f) return true;
return false;
}
使用标准模板库函数排序:
sort(a,a+n+1,cmp);
贪心算法:
void g(int n,action a[],bool b[])
{
b[1]=true;//第一个活动是必选的
int preend=1;
for(int i=2;i<=n;i++)
if(a[i].s>=a[preend].f)
{
b[i]=true;
preend=i;
}
}
在贪心算法中还有揹包问题、最优装载问题、删数问题、多出最优服务次序问题、选择鱼最多的湖钓鱼、反转照片··········
说实在的,在做贪心问题的题时,觉得这也太难了,但还是很耐心的一天做一道的做下来了 ,感觉不只学到了算法知识,也锻炼了自己的毅力,算是头一次耐心的去攻难题,虽然过程不尽如意,有很多题也是东拼西凑,在网上搜答案,借鉴别人的算法,但还是很有成就感大体开头还是好的,俗话说良好的开端是成功的一半,我这开端还是相当完美的 :
首先:我学到了贪心算法知识,在解ACM提的时候,贪心算法是个不错的选择;因为它的思路比较直接简单,写的程序也不是很长。一般只要想出来思路解题时非常简单的。而且它给出的题都是英文,也锻炼了英语阅读水平。
其次:贪心算法也开阔了我的解题思维,我感觉不只是在ACM的解题过程中,在平时做题时也可以采取算法里边的相关知识使意义简单,抓出题目要表达的本质以及要求;很多时候用这种方法很难解出来这道题,虽然看起来有头绪,但用另一种方法却很简单,当然这另一种方法和前一种思路是不同的,这时候我们要敢于打破关心思维,果断放弃之前的思路,不要怕麻烦,有时候看似麻烦却比一直停滞不前来得舒服,也就是说虽然换一种方法和前边相比是退了几步,但最终的收获却比之前大。
还有:我觉得对生活方面也有很多启发,比如做一件事情,怎样做费力最小;再比如生活中遇到的难题,换个思路或许很简单。
第三讲:搜索
这一讲主要讲的是广度优先和深度优先算法。
什么是搜索算法?
搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。
相比于单纯的枚举算法有了一定的方向性和目标性。算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态)。
搜索算法里的相关概念:
状态:对问题在某一时刻进展情况数学描述或数学抽象;每一个状态都会是答案的一个“可能的”解。状态的转移就是问题从一个状态转移到另一个状态,这样就可以进行搜索的一步步延伸,最后要得到的解也是其中的一个状态。
进行状态转移的两种算法:深度优先搜索和广度优先搜索。
广度优先搜索:
基本思想:从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。
生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。
具体过程:
1 每次取出队列首元素(初始状态),进行拓展
2 然后把拓展所得到的可行状态都放到队列里面
3 将初始状态删除
4 一直进行以上三步直到队列为空。
广度优先搜索框架:
While Not 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
深度优先搜索:
基本思想:从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。
具体实现过程
1 每次取出栈顶元素,对其进行拓展。
2 若栈顶元素无法继续拓展,则将其从栈中弹出。继续1过程。
3 不断重复直到获得目标状态(取得可行解)或栈为空(无解)。
深度优先搜索框架:
A、 递归实现:
Function Dfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs (Step + 1, Next ))
End
B、非递归实现:
非递归实现:
While Not Stack.Empty ()
Begin
Tmp = Stack.top()
从Tmp拓展下一个未拓展的状态Next
If 没有未拓展状态(到达叶节点) Then
Stack.pop()
Else If 状态Next合法 Then
Stack.push(Next)
End
主要例题分析:
1、帮助小明
题意:小明参加一个抢东西的电视节目。这个节目很有意思,一共有n个东西可以拿(n<50)每个参加活动的人不能拿重量超过m(m<2000)的东西,而每个东西都有它的价值v,有的高有的低,帮助小明安排要拿的东西,使总价值最高。
分析:这是一个0-1揹包问题。
对于每件物品,有两种选择:
1)拿这件物品(条件是最大重量不超过m)
2)不拿这件物品
2、滑雪问题:
题意:Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个 区域中最长底滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
思路:一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-…-3-2-1更长。事实上,这是最长的一路。
如果说做第一个专题时还算勉勉强强那第二个专题就说不过去了,只做了8道题,还是一开始比较简单的那几道,搜索类的题真的是比较麻烦,也比较考验人的耐心,总之我的耐心还是欠缺的,继续努力吧。总感觉自己对待事物太过随意了点,不愿意吃苦,成不了大事,但这也给了我教训,从来不愿意吃苦后来就要吃亏,一开始不努力后来努力就晚了,总之不要老是做让自己后悔的事,凡事要是选择去做了,几句不单单是坚持下来那么简单,要付出努力,要用心去做。
第四讲:动态规划
要是说贪心算法是打打闹闹,到了搜索就有点难了,那这个动态规划就是难上了新高度,但主要原因是上课没人这听讲吧,记得老师说过这个算法思路不难想,主要是写程序就行了。
什么是动态规划?
动态规划是解决多阶段决策问题的一种方法,就是一种排除重复计算的算法,更具体的说,动态规划就是用空间换取时间。它的指导思想:在做每一步决策时,列出各种可能的局部解;依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。以每一步都是最优的来保证全局是最优的。它的基本概念有:阶段:据空间顺序或时间顺序对问题的求解划分阶段。状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。决策:根据题意要求,对每个阶段所做出的某种选择性操作。状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。
动态规划的一般解题步骤:
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
关于多阶段决策问题。多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。
多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.这里的最优原理是指:不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。
最优决策序列的子序列,一定是局部最优决策子序列。
包含有非局部最优的决策子序列,一定不是最优决策序列。
主要来有以下方面:
1、最长上升序列。
2、最大字段和问题。
3、最大M子段和问题。
4、最大子矩阵和问题。
例题分析:
书稿问题
题意:假设有M本书(编号为1,2,…M),想将每本复制一份,M本书的页数可能不同(分别是P1,P2,…PM)。
任务:将这M本书分给K个抄写员(K<=M) 每本书只能分配给一个抄写员进行抄写,而每个抄写员所分配到的书必须是连续顺序的。
复制工作是同时开始进行的,并且每个抄写员复制一页书的速度都是一样的。所以,复制完所有书稿所需时间取决于分配得到最多工作的那个抄写员的复制时间。试找一个最优分配方案,使分配给每一个抄写员的页数的最大值尽可能小。
思路:设dp[i][j]表示前j个人复制前i本书所需要的最少时间,有状态转移方程
dp[i,j]=min(dp[i][j],max(dp[v][j-1],sum[v+1][i]))
其中1<=i<=m,1<=j<=k,j-1<=v<=i-1,
sum[v+1][j]表示第v+1本书到第i本书的页数之和。
具体实现代码:
const int MAXN = 510;
int sum[MAXN],path[MAXN],dp[MAXN][MAXN];
int main(){
int m,k,i,j,v,ca,p,t;
scanf(“%d”,&ca);
while(ca–){
scanf(“%d %d”,&m,&k);
for(sum[0]=0,i=1;i<=m;i++){
scanf(“%d”,&p);
sum[i]=sum[i-1]+p;
}
memset(dp,-1,sizeof(dp));
for(dp[0][0]=0,i=1;i<=m;i++)
for(j=1;j<=i && j<=k;j++){
if(j==1) dp[i][j]=sum[i];
else
for(v=j-1;v<=i-1;v++){
t=max(dp[v][j-1],sum[i]-sum[v]);
if(dp[i][j]==-1 || t<=dp[i][j])
dp[i][j]=t;
}
}
总之这一讲学的不好,看来回去还得好好学,我想可以利用假期的时间学一下这一块,去航电最最之前没写完的题,下学期再战,哈哈。不过教训也是有的:永远不要轻言放弃,坚持下来了,认真去做,就有收获,还有就是要学会与人合作,大家一起走,才会走的更远,总之,每个人都值得我们去尊重去学习,要好好团结同学,因为一个人学真的是动力不足啊,总之我发现了下伙伴的重要性。
第五讲:图算法
总之快到期末了,我也有了幡然悔悟的决心,新的算法也开始讲了,我决心要有一个完美的第五讲:课前认真看讲义,上课仔细听老师讲,课后刷题,总之继续努力呗。
首先要了解一下什么是树?
树 (Tree) 是 n (n≥0) 个结点的有限集。若 n = 0,称
为空树;若 n > 0,则它满足如下两个条件:
(1) 有且仅有一个特定的称为根 (Root) 的结点;
(2) 其余结点可分为 m (m≥0) 个互不相交的有限集
T1, T2,T3, …, Tm,其中每一个集合本身又是一棵树,
并称为 根的子树 (SubTree)。
然后是了解一下什么是图?
图 (Graph) 是一种复杂的非线性数据结构,由顶
点集合及顶点间的关系(也称弧或边)集合组成。可
以表示为: G=(V, {VR})
其中 V 是顶点的有穷非空集合; VR 是顶点之间关系
的有穷集合,也叫做弧或边集合。弧是顶点的有序对,
边是顶点的无序对。
图的存储结构有两种——
第一种是数组表示法:
对于一个具有n个顶点的图,可用两个数组存储。其中一个一维数组存储数据元素(顶点)的信息,另一个二维数组(图的邻接矩阵)存储数据元素之间的关系(边或弧)信息。
另一种是邻接表法:
顶点数很多n>1000,边数却不多。采用邻接表存储后,很多算法的复杂度也都是跟边数有关。连通性的问题很多情况边数不多,多采用邻接表存储方式。
图算法里的一个重要概念;并查集。
并查集的概念:将编号分别为1…N的N个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
1、合并两个集合
2、查找某元素属于哪个集合
例题分析:
畅通工程
题意:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
参考代码:
#include “stdio.h”
int bin[1002];
int findx(int x)
{
int r=x;
while(bin[r] !=r)
r=bin[r];
return r;
}
void merge(int x,int y)
{
int fx,fy;
fx = findx(x);
fy = findx(y);
if(fx != fy)
bin[fx] = fy;
}
int main()
{
int n,m,i,x,y,count;
while(scanf(“%d”,&n),n)
{
for(i=1;i<=n;i++)
bin[i] = i;
for(scanf(“%d”,&m);m>0;m–)
{
scanf(“%d %d”,&x,&y);
merge(x,y);
}
for(count=-1, i=1;i<=n;i++)
if(bin[i] == i)
count ++;
printf(“%d\n”,count);
}
}//总之这个模板很重要
图算法的经典应用:最小生成树
两种方法:A) Prim 算法;B) Kruskal算法
A)prim算法的基本思想:任取一个顶点加入生成树;在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树,重复上一步骤,直到所有的顶点都进入了生成树为止
Prim算法
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件iÎS,jÎV-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。
B)kruskal算法的基本思想
对所有边从小到大排序;依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;直到生成树中有了n-1条边,即告终止。算法的时间复杂度O(eloge)
Kruskal算法:
将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。
最终得到的结果就是最小生成树。
并查集
图算法这一模块还在继续,希望有一个好的模块···
总结:经过一个学期的ACM的课程的学习,我觉得自己的思维更开阔了,学到了很多新的知识,也学到了很多道理,当然对ACM的课程也是又爱又恨,总之这不是结束,我还会继续努力的···