标签:
学了近一学期的acm,从开始什么都不懂到逐渐懂得用算法解决问题,从不顾时间性能和空间性能到开始考虑怎样减少时间复杂度和空间复杂度问题,正如老师所说的‘你选这门课,收获的不仅是学分,更重要的是你会发现你的思想,考虑问题的角度都会得到提升’。
从一开始学的贪心算法到搜索再到动态规划,最后是图论。每一种算法都会收获不一样的感悟,贪心算法顾名思义贪心,每次选最短的时间或其他 ;搜索么就是遍历,分为广度和深度两种搜索算法,就是从上到下一直找,选用哪种算法还要根据具体问题具体分析;动态规划感觉和贪心算法有异曲同工之处,都很“贪心”,动态规划是在每一个决策阶段选取最优解,然后和下一个阶段联系起来,在比较选取最优解,动态规划和搜索相比,一个比较难想出来,但一旦想出来就特别好解决,代码特别短;另一个比较容易想,但代码却特别长,所以我做题能用动态规划绝不用搜索;最后的图论有各种算法,例如prim算法,kruskal算法等,每一种的思想都很独特,做题中一定要明确用哪种算法比较省时,省空间。
有时明明感觉代码写的很对,实现也可以实现,但一提交有时就会wrong answer或者是各种error,然后改好长时间找出来错误会发现其实只是因为一个小错误导致的;但有时也可能是超时问题,这时就要考虑换一种算法实现,做题中好长时间都提交错误突然在提交发现ac了那心情真是不可言说的愉悦。
对贪心算法的理解:
贪心算法就是在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解的方法。
从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。
贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。
这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。
利用贪心策略解题,需要解决两个问题:
(1)该题是否适合于用贪心策略求解;
(2)如何选择贪心标准,以得到问题的最优/较优解。
使用贪心算法求解问题应该考虑如下几个方面:
(1)候选集合A:为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。
(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
(5)可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。
贪心算法大概有下面几种经典类型:
1:活动安排问题 这种问题可以用结束时间排序,然后不断拿后一次的开始时间比较,循环,最大利用时间。。
2,:背包问题 背包问题不能单纯的考虑一方面,重量或是价格,应用一个词“性价比”来装包,从性价比最高的开始,一直装到不能再装为止,如果能分割,那么可以最大利用空间!!
3:最优装载问题 这个和背包问题差不多,甚至还简单,就不说了。。
4:多次服务最优次序问题:这种类型还是比较简单的,首先排好序,然后两个窗口依次取,谁完成就取,从小的开始。。
5:桌子移动问题:其实这个可以把桌子数除以二,然后可以把数组值为零,依次输入数据,从谁那通过就加一,依次累加即可。。
贪心算法,实际应用我感觉应该还是挺有用的,因为他可以把一个大的问题分解为若干个小问题,然后求最优解,这样比求一个大问题肯定要简单的多,毕竟有句话是“大事化小,小事化了”吗。。
对搜索算法的理解:
那么什么是搜索算法呢?
搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。
相比于单纯的枚举算法有了一定的方向性和目标性。算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态)。状态(state)是对问题在某一时刻进展情况的数学描述,或者是数学抽象。每一个状态都会是答案的一个“可能的”解。状态的转移就是问题从一个状态转移到另一个状态,这样就可以进行搜索的一步步延伸,最后要得到的解也是其中的一个状态。
基本搜索可以分为两种,分别是广度搜索和深度搜索。另外还有二分法三分法我们一会再说。
广度搜索的基本思想:从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。
生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。
可以用队列实现!
广度优先算法的基本框架是:
/*
*
*
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
深度搜索的基本思想:从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。
可以用栈实现!
深度优先算法的递归实现框架:
/*
*
*
Function Dfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs (Step + 1, Next ))
End
*
*
*/
非递归实现的框架:
/*
*
*
While Not Stack.Empty ()
Begin
Tmp = Stack.top()
从Tmp拓展下一个未拓展的状态Next
If 没有未拓展状态(到达叶节点) Then
Stack.pop()
Else If 状态Next合法 Then
Stack.push(Next)
End
*
*
*/
二分法:
简单定义:在一个单调有序的集合中查找元素,每次将集合分为左右两部分,判断解在哪个部分中并调整集合上下界,重复直到找到目标元素。
时间复杂度:O (logn),优于直接顺序查找O(n)
参考模板:
/*
*
*
//x:待查找的元素, n:数组集合大小, num数组单调递增
intlow=0,high=n,mid,res = -1; //low:集合下界 high:集合上节
while(low<=high)
{
mid=(low+high)/2; //mid:将集合分割为两部分
if(num[mid]==x) //查找到符合元素x
{
res = mid;
break;
}
else if(num[mid]<x) //x在右边部分,调整集合下界
low=mid+1;
else //x在左边部分,调整集合上界
high=mid-1;
}
*
*
*/
二分的思想在很多领域中都得到了广泛的应用,是很多算法的基础。相信不难理解整个二分查找的框架和用法。
三分法其实和二分法差不多。
而什么时候需要三分呢?
当需要求某凸性或凹形函数的极值,通过函数本身表达式并不容易求解时,就可以用三分法不断逼近求解。
基本模板:
/*
*
*
double mid, midmid;
while ( low + eps < high )
{
mid= (low + high) / 2;
midmid = (mid + high ) / 2;
double cmid = cal(mid);
double cmidmid = cal(midmid);
if( cmid > cmidmid )
high = midmid;
else
low = mid;
}
*
*
*/
对于求解一些实际问题,当公式难以推导出来时,二分、三分法可以较为精确地求解出一些临界值,且效率也是令人满意的。
灵活应用这些方法对解题会很有帮助。
对动态规划的理解:
那么什么是动态规划呢?
动态规划是解决多阶段决策问题的一种方法。
动态规划实际上就是一种排除重复计算的算法,更具体的说,动态规划就是用空间换取时间。
多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。
多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.
最优性原理:
不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。
最优决策序列的子序列,一定是局部最优决策子序列。
包含有非局部最优的决策子序列,一定不是最优决策序列。
动态规划的指导思想:
在做每一步决策时,列出各种可能的局部解
依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。
以每一步都是最优的来保证全局是最优的。
动态规划问题具有以下基本特征:
问题具有多阶段决策的特征。
每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”。
每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态。
每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。
动态规划的几个概念:
阶段:据空间顺序或时间顺序对问题的求解划分阶段。
状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。
决策:根据题意要求,对每个阶段所做出的某种选择性操作。
状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。
动态规划的一般解题步骤:
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
区间DP:
区间动态规划问题一般都是考虑,对于每段区间,他们的最优值都是由几段更小区间的最优值得到。将一个区间问题不断划分为更小的区间直至一个元素组成的区间,枚举他们的组合,求合并后的最优值。
区间dp的基本模板:
/*
*
*
for(intp = 1 ; p <= n ; p++){//p是区间的长度,作为阶段
for(int i = 1 ; i <= n ; i++)
{//i是穷举区间的起点
intj = i+p-1;//j为区间的终点
for(intk = i ; k < j ; k++)//状态转移
dp[i][j]= min{dp[i][k]+dp[k+1][j]+w[i][j]};
//这个是看题目意思,有的是要从k开始不是k+1
dp[i][j]= max{dp[i][k]+dp[k+1][j]+w[i][j]};
}
}
*
*
*/
最后是对图论的理解:
图是由顶点集合以及顶点间的关系的集合组成的一种关系的数学表示
G = (V,E)
其中顶点是由有穷非空集合
顶点之间的关系(边)是有穷集合
Path (x , y)表示从x到y的一条单向通路,它是有方向的
图分为三类:
有向图:图中的边是有方向的。E (x ,y) 和E ( y ,x)表示的边不同
无向图:图中的边是没有方向的。
完全图:n个顶点的图两两连边,即有 n(n-1)/2条边,则此图为n的完全图。 用K n表示。
图的一些概念:
邻接顶点:如果(u ,v) ∈E(G),则称u与v互为邻接顶点
子图:设有两个图G(V,E)和G’(V’,E’),若V’ V 且E’E’,则称图G‘是图G的子图
权:某些图的边上标有相关的数字,称为该边的权值
顶点的度 一个顶点V的度是与它相连边的条数,记为Deg(V).
顶点的入度 一个顶点V的入度是以它为终点有向边的条数,记为InDeg(V)
顶点的出度 一个顶点V的入度是以它为起点有向边的条数,记为OutDeg(V)
路径 在图 G=(V, E) 中, 若从顶点 vi 出发, 沿一些边经过一些顶点 Vp1, Vp2, …, Vpm,到达顶点Vj。则称顶点序列 (Vi Vp1 Vp2 ... Vpm Vj) 为从顶点Vi 到顶点 Vj 的路径。它经过的边(Vi, Vp1)、(Vp1, Vp2)、...、(Vpm, Vj) 应是属于E的边。
下面介绍几种算法:
单源最短路径
Dijkstra
Bellman-Ford
SPFA
任意两点间
Floyd
他们的复杂度是:
Dijkstra: O(V2)或O(E+VlgV)
Bellman-Ford: O(EV)
Floyd: O(V3)
SPFA: 期望复杂度O(E)
下面列几种常用算法到的模板:
Prim算法:
(1) 任意选定一点s,设集合S={s}
(2) 从不在集合S的点中选出一个点j使得其与S内的某点i的距离最短,则(i,j)就是生成树上的一条边,同时将j点加入S
(3) 转到(2)继续进行,直至所有点都己加入S集合。
/*
*
*
int prim(int s)//s为初始加入的点
{
inti,j,sum=0;
for(i=1;i<=n;i++)
closest[i]=10000000;
for(i=1;i<=n;i++)
closest[i]=map[s][i];
closest[s]=0;
intnow;
for(i=1;i<n;i++)
{
intmin=INT_MAX;
for(j=1;j<=n;j++)
if(closest[j]&&closest[j]<min)
{
min=closest[j];
now=j;
}
sum+=min;
closest[now]=0;
for(j=1;j<=n;j++)
if(map[now][j]&&map[now][j]<closest[j])
closest[j]=map[now][j];
}
returnsum;
}
*
*
*/
kruscal算法:
对所有边从小到大排序;
依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;
直到生成树中有了n-1条边,即告终止。
算法的时间复杂度O(eloge)
/*
struct Edge
{
intv1,v2,w;
}edge[M],tree[M]; //w为v1顶点到v2顶点的边权
/ *
int Find (int parent[],int u)//第1种写法
{
inttmp = u;
while (paren[tmp] != -1)
tmp = parent[tmp];
return tmp;
}
*/
int Find (int u) //第2种写法
{
if(u != parent[u])
parent[u] = Find(paren[u]);
return parent[u];
}
bool cmp (Edge a,Edge b)
{
return a.w < b.w;
}
int Kruskal()//parent[]表示集合
{
intparent[M];
inti,j,sum,vf1,vf2;
sort(edge,edge+E,cmp);
// memset(parent,-1,sizeof(parent));//对应第1种并查集的初始化
for(i = 0;i < n;i ++) //对应第2种并查集的安始化
parent[i] = i;
sum= i = j = 0;
while (i < E && j < N - 1)//生成的边数为N-1
{
vf1 = Find(parent,edge[i].v1); //找这两个点的祖先
vf2 = Find(parent,edge[i].v2);
if (vf1 != vf2) //若两个点的祖先不同,说明不在同一集合
{
parent[vf2] = vf1; //把vf2点加到vf1点的集合中
tree[j++] = edge[i]; //把边加到tree[]数组中,这句题目没要求可忽略之
sum += edge[i].w; //sum 加上其边权
}
i ++;
}
return sum;
}
*
*
*/
Dijkstra算法:
Dijkstra算法解决了有向图G=(V,E)上带权的单源最短路径问题,但要求所有边的权值非负。Djikstra算法中设置了一顶点集合S,从源点s到集合中的顶点的最终最短路径的权值均已确定,算法反复选择具有最短路径估计的顶点u∈V-S,并将u加入S中,对u的所有出边进行松弛。
/*
*
*
int dijkstra(int s,int n)
{
inti,j;
intv[MAX];
intd[MAX];
for(i=1;i<=n;i++)
d[i]=map[1][i];
v[1]=1;
for(i=1;i<n;i++)
{
intnow,min=INT_MAX;
for(j=1;j<=n;j++) //选最小的加入集合
{
if(!v[j]&&min>d[j])
{
min=d[j];
now=j;
}
}
v[now]=1;
for(j=1;j<=n;j++) //松弛
if(!v[j]&&d[now]+map[now][j]<d[j])
d[j]=d[now]+map[now][j];
}
return0;
}
*
*
*/
另外并查集应该算重要的解决问题的方法,并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。
初始化
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式时间 复杂度均为O(N)。
查找
查找元素所在的集合,即根节点。
合并
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
这就是所有的算法了,前前后后也算是有一个学期的,其间也曾想过要放弃,因为差不多至少要两天一个题,太累,因为还有其他许多事要做,所以很纠结,不想放弃acm,所以中间有一段时间也算是偷了懒,不过后来还是坚持下来了,因为这不仅是对于学习的态度,更是对人生的态度,试想如果一件事连坚持都坚持不了,他又能办成什么事情呢,另外学习acm不仅收获了知识,还收获了友情,因为有些不会的题你需要和别人探讨一下,我相信在这学习完acm后,会对以后产生或多或少的帮助,所以谢谢老师,共勉!!!
标签:
原文地址:http://blog.csdn.net/xu_acm/article/details/51848101