ACM总结

ACM课很难,这在上之前就有所耳闻,但依然抱有一丝希望,直到真正上了这门课。。。。。。。。。。。。。。虽然难,但我收获了很多。坚持下来,十分不易。比必修课还费劲。但对编程语言有了更深的了解,同时还对算法有了初步的了解。下面就是我对这门课程的一些简单总结。

1,贪心算法

算是第一次正式接触的ACM算法,刚开始理解起来挺难得,后来仔细看了课件,顿时有种“原来如此”的感叹。这时候刚刚开始接触,每次AC一道题,心里还是很激动的。

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

贪心法的基本思路:

从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。

实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
   求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;

例题分析

1、[背包问题]有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。

要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

物品

A  

B  

C

D

E

F

G

重量wi

35   

30

60

50

40

10

25

价值 pi

10

40

30

50

35

40

30

分析:

目标函数: ∑pi最大

约束条件是装入的物品总重量不超过背包容量:∑wi<=M(M=150)

(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?

(2)每次挑选所占重量最小的物品装入是否能得到最优解?

(3)每次选取单位重量价值最大的物品,成为解本题的策略。 ?

值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。

贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。

可惜的是,它需要证明后才能真正运用到题目的算法中。

一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。

对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:

(1)贪心策略:选取价值最大者。反例:

W=30

物品:A  B  C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。

(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。

(3)贪心策略:选取单位重量价值最大的物品。反例:

W=30
物品:A  B  C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。

所以需要说明的是,贪心算法可以与随机化算法一起使用,具体的例子就不再多举了。(因为这一类算法普及性不高,而且技术含量是非常高的,需要通过一些反例确定随机的对象是什么,随机程度如何,但也是不能保证完全正确,只能是极大的几率正确)

 2.搜索

DFS:深度优先搜索算法,正如算法名称那样,深度优先搜索所遵循的搜索策略是尽可能“深”地搜索图。在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续汉下去。当结点v的所有边都己被探寻过,搜索将回溯到发现结点v有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被发现为止。

BFS:广度优先搜索算法,是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法(后续有介绍)都采用了和宽度优先搜索类似的思想。他属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。它之所以称之为宽度优先算法,是因为算法自始至终一直通过已找到和未找到顶点之间的边界向外扩展,就是说,算法首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+l的其他顶点。

下面介绍连连看典型BFS例题:

简单题意:规则是在一个棋盘中,放了很多的棋子。如果某两个相同的棋子,可以通过一条线连起来(这条线不能经过其它棋子),而且线的转折次数不超过两次,那么这两个棋子就可以在棋盘上消去。连线不能从外面绕过去的,玩家鼠标先后点击两块棋子,试图将他们消去,然后游戏的后台判断这两个方格能不能消去。

思路分析:首先明确这是一个搜索的问题,因为只要找到解决的方法即可,就考虑用广度搜索, 建立一个队列,用方向数组将方向记录下来。在运用广度搜索的办法将每一级的所有可能情况压入队列,在判断是否到达最终条件即可。

代码如下:#include <iostream>  

#include <queue>  

#include <cstring>  

using namespace std;  

struct node  {  

    int x, y;  

    int t, d;  };  

queue<node> q;  

int n, m, map[1002][1002], prove;  

int visit[1002][1002][4];   

int qry, sx, sy, ex, ey;  

int dx[4] = {0, -1, 0, 1};  

int dy[4] = {1, 0, -1, 0};  

int check(int x, int y)  {  

    if(x < 1 || x > n || y < 1 || y > m)//方向数组记录方向  

        return 0;  

    else  

        return 1;  }  

void bfs()  {  

    while(!q.empty())  

        q.pop();  

    memset(visit, 0, sizeof(visit));  

    node s, e;  

    s.x = sx;  s.y = sy;  

    s.t = 0;   s.d = -1;  

    q.push(s);  

    while(!q.empty())  {  

        s = q.front();  

        q.pop();  

        if(s.t > 2)  

            continue;  

        if(s.x == ex && s.y == ey)//最终成立的条件    {  

            prove = 1;  

            cout << "YES" << endl;  

            break;    }  

        for(int i = 0; i < 4; i++)  {  

            e.x = s.x + dx[i];  e.y = s.y + dy[i];  

            if(!check(e.x, e.y) || visit[s.x][s.y][i])    

                continue;  

            if( map[e.x][e.y] == 0 || (e.x == ex && e.y == ey) )   {  

                if(s.d == -1 || i == s.d)   {  

                    e.d = i;  

                    e.t = s.t;  

                    q.push(e);  

                    visit[s.x][s.y][i] = 1;     }  

                else   {  

                    e.d = i;  

                    e.t = s.t + 1;  

                    q.push(e);  

                   visit[s.x][s.y][i] = 1;  

                }    }   }   }  }  

int main()  {  

    while(cin >> n >> m)  {  

        if(!n && !m)  

            break;  

            int i=1;  

            int j=1;  

  

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

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

            cin >> map[i][j];  

        cin >> qry;  

        for(i=1; i<= qry;i++)   {  

            cin >> sx >> sy >> ex >> ey;  

            prove = 0;  

            if(map[sx][sy] == map[ex][ey] && map[sx][sy] != 0)  

               bfs();  

  

            if(!prove)  

                 cout << "NO" << endl;   }   }  } 

3,动态规划

动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。

动态规划问题运算量比较大,通常提前列出问题的所有可能并保存到表中,在根据键值查表,这种方法的特点是牺牲空间来换取时间,举个简单的例子,斐波拉契数列,求f(10),需要11次运算,求f(11),需要12次运算,如果用公式f(n) = f(n-1) + f(n-2)分开求,需要计算11+12=23次,但如果将将f(10)保存,计算f(11),只需在f(10)的基础上再运算一次,即11+1=12次。可见,这种做法非常节省时间。

动态规划问题,相较于贪心和搜索来说,一个显著的特点就是代码量很少,一个贪心或者搜索问题,一般都会有五六十行代码,而一个DP问题的代码量,往往就十几行甚至更短,以至于真正的核心就那么一行递推公式。但这并不代表DP问题就简单,做DP问题需要很强的逻辑思维和思考能力(思路很重要,思路好的时候,一天可以AC 5道题,要是思路不好,一天可能一道题也做不出来),需要一颗具备把逻辑问题转化为特征方程能力的大脑,总之,做DP问题,是真正用“脑子”做题,不是像搜索一样,靠“模板”来嵌套。

4,图论

先说明两个图的存储方式邻接矩阵和邻接表,邻接矩阵的使用场合为:数据规模不大n <= 1000,m越大越好、稠密图最好用邻接矩阵、图中不能有多重边出现。

邻接表的基础代码为struct edge

{

    int x, y, nxt; typec c;

} bf[E];

 

void addedge(int x, int y, typec c)

{

    bf[ne].x = x; bf[ne].y = y; bf[ne].c = c;

    bf[ne].nxt = head[x]; head[x] = ne++;

}

并查集:将编号分别为1…N的N个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。常见两种操作:合并两个集合,查找某元素属于哪个集合。

最小生成树问题之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的一棵最小生成树。

基础代码:int prim(int n,int mat[][MAXN],int* pre){

int min[MAXN],ret=0;

int v[MAXN],i,j,k;

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

min[i]=inf,v[i]=0,pre[i]=-1;

for (min[j=0]=0;j<n;j++){

for (k=-1,i=0;i<n;i++)

if (!v[i]&&(k==-1||min[i]<min[k]))

k=i;

for (v[k]=1,ret+=min[k],i=0;i<n;i++)

if (!v[i]&&mat[k][i]<min[i])

min[i]=mat[pre[i]=k][i];

}

return ret;

}

最小生成树问题之Kruskal算法:

基本思想:将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。最终得到的结果就是最小生成树。并查集。

基本内容:把原始图的N个节点看成N个独立子图;每次选取当前最短的边,看两端是否属于不同的子图;若是,加入;否则,放弃;循环操作该步骤二,直到有N-1条边;一维数组,将所有边按从小到大的顺序存在数组里面先把每一个对象看作是一个单元素集合,然后按一定顺序将相关联的元素所在的集合合并。能够完成这种功能的集合就是并查集。对于并查集来说,每个集合用一棵树表示。它支持以下操作:Union (Root1, Root2) //合并两个集合;Findset(x) //搜索操作(搜索编号为x所在树的根)。树的每一个结点有一个指向其父结点的指针。

基础代码:void MakeSet()

{

    long i;

    for (i=0;i<=m;++i)

    {

        father[i]=i;

    }

}

long Find(long i)

{

    long r=i;

    while (father[r]!=r)

    {

        r=father[r];

    }

    while (father[i]!=r)

    {

        long j=father[i];

        father[i]=r;

        i=j;

    }

    return r;

}

以上就是对这学期的先要总结。看似很多,其实相较于ACM比赛来说,这只是九牛一毛,这之后,还有很长的路要走 ,共勉。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值