ACM程序设计节课总结

1 浅谈ACM

刚进入大学时,我便对C++这门课程感到十分有兴趣,在上学期的C++程序设计课程中,我便迷上了这门语言,以及十分有个性的费老师,可能是有那么一点灵性吧,我入门的非常快,一开始放在农大OJ上的题目能非常快的做出来,在那时体会到了AC的快感。不过在我现在看来,那时的我还是太天真了,在大一上学期,我所学的只能算是C++这门编程语言的皮毛,可能连入门都不算,当时的自大在我现在看来就是无知,就像是学会了点气功就沾沾自喜,殊不知还有更高深奥妙的功法,将这门课的学习比作是学习武功真是再恰当不错了。所以为了让自己变得更强,我在大一下学期一咬牙,报了这门在学长学姐口中有些恐怖的课程,说实话,这门课程是真的有难度,不过难度是层层递进的,从最基础的STL算法到递归递推再到动态规划,是有一个逐渐上升的过程,老师布置在网上的题目有水题也有难题,都是有考验能力与增加做题量的用途。

2 ACM学习心得

ACM课程设计这门选修课本质上就是一个学习算法和思想的过程,不同的专题都有不同的学习方法,下面是我学习个个专题的心得体会。

2.1 STL专题

STL,即C++标准模板库,是我接触ACM的第一个知识,STL是C++标准程序库的核心,深刻影响了标准程序库的整体结构,STL由一些可适应不同需求的集合类(collection class),以及在这些数据集合上操作的算法(algorithm)构成。STL内的所有组件都由模板(template)构成,其元素可以是任意类型,STL是所有C++编译器和所有操作平台都支持的一种库,简单来说,就是使许多操作变得简单了许多,比如学STL之前,数组用的还是简单的一维二维数组,在定位与查找方面有诸多的不便,而STL中有迭代器,对于STL中的所有容器都提供获得迭代器的函数,能非常方便的定位到所想寻找的元素位置,以一个list为例

list<int> l;

for(pos=l.begin();pos!=l.end();++pos{

 }

在省略处便可输入寻找条件的代码,而在普通的数组中对于一些特殊的情况则难以实现。

Vector数组则非常方便的解决了定义一个数组时所要面对的要开的大小的问题,vector相当于一个动态的一维数组,可以随着填入的数据即时扩大容量,而且vector的元素可以是任意类型T,支持随机存取,若要使用vector,必须包含头文件#include<vector>,相较于普通的一维数组,vector还有一个好处,vector可以使用size和capacity函数,能够返回动态数组的实际元素个数以及能容纳的元素最大数量。

Map/multimap容器,可以类似于一个动态的即时扩大容量的二维数组,使用平衡二叉树管理元素,元素包含两部分(key,value),key和value可以是任意类型,而使用map/multimap前必须包含头文件#include<map>,map/multimap根据元素的key自动对元素进行排序,因此根据元素的key进行定位非常的快,但根据元素的value定位很慢,不能直接改变元素的key,可以通过operator[]直接存取元素值,还有一点map中不允许key相同的元素,multimap允许key相同的元素。Map中有许多非常好用的查找元素的函数,例如count(key):返回键值等于key的元素个数,find(key):返回键值等于key的第一个元素。

而virtual judge上的《ACM程序设计》书中题目B遇到map马上就迎刃而解,下面是该题:

We all know that FatMouse doesn't speak English. But now he has to be prepared since our nation will join WTO soon. Thanks to Turing we have computers to help him.

Input Specification

Input consists of up to 100,005 dictionary entries, followed by a blank line, followed by a message of up to 100,005 words. Each dictionary entry is a line containing an English word, followed by a space and a FatMouse word. No FatMouse word appears more than once in the dictionary. The message is a sequence of words in the language of FatMouse, one word on each line. Each word in the input is a sequence of at most 10 lowercase letters.

Output Specification

Output is the message translated to English, one word per line. FatMouse words not in the dictionary should be translated as "eh".

 这道题大致的意思就是输入翻译表,每行的左边单词是英语单词,每行右边的单词是一个fat mouse 语言,所以只要用map就可以解决这个翻译问题可以把fat mouse 语作为键值,对应的英语翻译作为元素值,主要问题是如何将输入翻译表和输出翻译分隔开,我用了两个getchar实现了这个操作,又因为防止每一行的首字母被吞做了一个make_ pair(b,e+a)的语句。所以设置了一个名为dictionary的map数组,输入由下列关键伪代码实现:

while(cin>>fat mouse语>>对应英语翻译)

{

d=getchar();

往dictionary中插入元素(make_pair(英语翻译,e+fatmouse语));

e=getchar();

if(e=='\n')break;

}

Set/multiset用法与map/multimap差不多,map容器是键-值对的集合,好比以人名为键的地址和电话号码。相反地,set容器只是单纯的键的集合。当我们想知道某位用户是否存在时,使用set容器是最合适的。

2.2 递归地推

    一个问题的求解需一系列的计算,在已知条件和所求问题之间总存在着某种相互联系的关系;如果可以找到前后过程之间的数量关系,能使复杂运算化为若干步重复的简单运算,充分发挥出计算机擅长于重复处理的特点。

递推算法的首要问题是得到相邻的数据项间的关系(即递推关系)。即把一个复杂的问题的求解,分解成了连续的若干步简单运算。

做完virtual judge上的递归递推这套练习后,还是有所心得的,做递推递归类的题目我觉得唯一的需要的是灵感和努力,灵感是因为做递推递归的练习时有时候要将题目与以前数学学过的思考题相结合,灵感便是指如何在看到题的几分钟里能够将题目解剖,取其主干然后与所认识的数学题目联系起来。努力指的是如果一时间内没办法想到以前见过的数学题目,那就想办法枚举出题目数据的前几项,然后找规律。这个过程肯定是枯燥且很难成功的。没什么特别好的办法,大概只能多看看题目了吧。

以一个题目为例:有一对夫妇买了一头母牛,它从第2年起每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?

这道题主要是得算出第n年出生的母牛数量,可以由题目推出递推公式,第n年出生的母牛来自第n-1年的牛和n-3年刚出生的小母牛成熟后所生的数量之和。下面是实现的关键代码:

int f(int a)

{

if(a<=4&&a>0)return 1;

    else return f(a-3)+f(a-1);

}

在做递归递推的时候,第一大关是得出递推递归公式,第二大关也是最容易忽略的地方就是超时问题,在需要多次递归的时候,有时候就会遇到超时的问题,而这种问题,需要做一些记忆化搜索就能避免超时。下面是一个简单的例题:

给定一个函数 f(a, b, c):

如果 a ≤ 0 或 b ≤ 0 或 c ≤ 0 返回值为 1;

如果 a > 20 或 b > 20 或 c > 20 返回值为 f(20, 20, 20);

如果 a < b 并且 b < c 返回 f(a, b, c−1) + f(a, b−1, c−1) − f(a, b−1, c);

其它情况返回 f(a−1, b, c) + f(a−1, b−1, c) + f(a−1, b, c−1) − f(a-1, b-1, c-1)。

其他没有什么特殊的地方,就是要注意一个记忆化搜索:

int f(int a,int b,int c)

{

if(a<=0||b<=0||c<=0)return 1;

else if(a>20||b>20||c>20)return f(20,20,20);

 if(m[a][b][c]==0)//判断是否已经计算过这里的函数值,若计算过则直接输出,未计算则进行计算。

 {

  if(a<b&&b<c)return m[a][b][c]=f(a,b,c-1)+f(a,b-1,c-1)-f(a,b-1,c);

            else return m[a][b][c]=f(a-1,b,c)+f(a-1,b-1,c)+f(a-1,b,c-1)-f(a-1,b-1,c-1);

 }

 else return m[a][b][c];

}

2.3 动态规划

第一次学ACM程序设计感到困难的第一大关就是动态规划,动态规划是解决多阶段决策问题的一种方法。多阶段决策问题就是如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都须作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果。动态规划遵从一个最优性原理,不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。最优决策序列的子序列,一定是局部最优决策子序列。包含有非局部最优的决策子序列,一定不是最优决策序列。

动态规划的指导思想是在做出每一步决策时,列出各种可能的局部解,然后依据某种判定条件,舍弃那些肯定不能得到最优解的局部解,并且以每一步都是最优的来保证全局是最优的。

动态规划与递归递推有些相似,但是动态规划多了一个要求,要求做最优的选择,在每次递推中选择最优的状态。动态规划问题的一般解题步骤分为六步:

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

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

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

4、找出边界条件。

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

6、递推求解。

例如virtual judge上动态规划的一道最长上升子序列的问题:一个数的序列bi,当b1 < b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)。我们的任务,就是对于给定的序列,求出最长上升子序列的长度。

    如何把这个问题分解成子问题呢?经过分析,发现“求以ak(k=1,2, 3…N)为终点的最长上升子序列的长度”是个好的子问题。

    由上所述的子问题只和一个变量相关,就是数字的位置。因此序列中数的位置k就是“状态”,而状态k对应的“值”,就是以ak做为“终点”的最长上升子序列的长度。这个问题的状态一共有N个。状态定义出来后,转移方程就不难想了。

假定MaxLen (k)表示以ak 做为“终点”的最长上升子序列的长度,那么:MaxLen (1) = 1,MaxLen (k) = Max { MaxLen (i):1<i < k 且 ai < ak 且 k≠1 } + 1,实际实现的时候,可以不必编写递归函数,因为从 MaxLen(1)就能推算出MaxLen(2),有了MaxLen(1)和MaxLen(2)就能推算出MaxLen(3)……

下面是实现动态规划的代码:

    int i,j,N;

    cin>>N;

    int b[N+1],aMaxLen[N+1];

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

    {

        cin>>b[i];

    }

    aMaxLen[1]=1;

    for(i=2;i<=N;i++)//求以第i个数为终点的最长上升子序列的长度

    {        

        int nTmp=0;//记录第i个数左边子序列最大长度

        for(j=1;j<i;j++)//搜索以第i个数左边数为终点的最长上升子序列长度

        {

 

            if(b[i]>b[j])

            {

                if(nTmp<aMaxLen[j])nTmp=aMaxLen[j];

            }

        }

        aMaxLen[i]=nTmp+1;

    }

    int nMax=-1;

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

    {

        if(nMax<aMaxLen[i])nMax=aMaxLen[i];

    }

    cout<<nMax;

2.4 二分三分查找算法

二分查找算法的简单定义:在一个单调有序的集合中查找元素,每次将集合分为左右两部分,判断解在哪个部分中并调整集合上下界,重复直到找到目标元素。使用二分查找法非常关键的一点是一个单调有序的集合,若不是单调有序,则需想办法转化成单调有序的,这个二分查找法的花费时间优于直接按顺序查找,实际使用的时候,则需将问题所指的东西即答案,如果答案具有特定的范围,并且验证答案是否成立的函数具有单调性。则可以在范围内对答案进行二分查找,从而快速确定答案。

这个专题,由于初高中介绍过类似的数学思想,在接受起来比动态规划要好很多,在二分贪心中的一个题目十分的经典:

Farmer John has built a new long barn, with N (2 <= N <= 100,000) stalls. The stalls are located along a straight line at positions x1,...,xN (0 <= xi <= 1,000,000,000).

 

His C (2 <= C <= N) cows don't like this barn layout and become aggressive towards each other once put into a stall. To prevent the cows from hurting each other, FJ want to assign the cows to the stalls, such that the minimum distance between any two of them is as large as possible. What is the largest minimum distance?

这道题大致的意思是一个农夫有n个牛舍,每个牛舍在自己的坐标xi,把c头牛放在这n个牛舍里面,求最近两头牛之间的最大距离。在这种求距离里的最大距离用二分法判断即可。以下是该题二分算法的关键实现代码:

bool fun(int mid)

{

    int cnt=1;

    int m=x[1];

    for(int i=2; i<=N; i++)

    {

        if(x[i]-m>=mid)

        {

            cnt++;

            m=x[i];

        }

        if(cnt>=C)

            return true;

    }

    return false;

}

而三分查找法则是二分查找法的进阶,随实际问题而使用,三分查找法就是将一组单调的先用一次二分查找找到一个mid,然后在mid和极值点再找一个mid2,不停的缩小范围,直到得出结果。

2.5 贪心算法

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

从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。老师说,贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。以下便是贪心算法的一般流程:

//A是问题的输入集合即候选集合

Greedy(A)

{

S={ };//初始解集合为空集

while (not solution(S))//集合S没有构成问题的一个解

{

x = select(A);//在候选集合A中做贪心选择

if feasible(S, x)//判断集合S中加入x后的解是否可行

S = S+{x};

A = A-{x};

}

return S;

2.6 背包问题

背包问题就是给定一个载重量为M的背包,考虑n个物品,其中第i个物品的重量 ,价值wi (1≤i≤n),要求把物品装满背包,且使背包内的物品价值最大。背包问题分为两类(根据物品是否可以分割),如果物品不可以分割,称为0—1背包问题(使用动态规划);如果物品可以分割,则称为背包问题(使用贪心算法)。

解决背包问题的时候基本都要创建一个数据结构体bag:

struct bag

{

    int w; //物品的重量

    int v; //物品的价值

    double c; //性价比

}a[1001]; //存放物品的数组

//形参n是物品的数量,c是背包的容量M,数组a是按物品的性价比降序排序

double knapsack(int n, bag a[], double c)

{

double cleft = c;        //背包的剩余容量

int i = 0;

double b = 0;          //获得的价值

//当背包还能完全装入物品i

while(i<n && a[i].w<cleft)

{

cleft -= a[i].w;

b += a[i].v;

i++;

}

//装满背包的剩余空间

if (i<n) b += 1.0*a[i].v*cleft/a[i].w;

return b;

}

2.7 搜索专题

    搜索分广度优先搜索和深度优先搜索,这两者的区别可以用一张图来分别,

广度优先搜索就是先将本行的所有情况遍历,再去遍历下一行,直到搜索到结果。而深度优先搜索是将一列所有的子状态遍历之后才会队同一层的下一个子状态进行遍历。这种算法可以在很多地方用到,是一种思想,不过在运用这种思想的时候要注意时间的问题,很容易超时。

2.8 图论

图论这一章是这学期最后所学的内容,将图与定义相结合,图论的内容就不会变得那么抽象,graph=(V,E)。V是一个非空有限集合,代表顶点(结点),E代表边的集合。

图也有遍历,并且可用广度优先搜索和深度优先搜索,从图中某一顶点出发系统地访问图中所有顶点,使每个顶点恰好被访问一次,这种运算操作被称为图的遍历。为了避免重复访问某个顶点,可以设一个标志数组visited[i],未访问时值为false,访问一次后就改为true。

图论中有最短路径算法:

初始化:点u、v如果有边相连,则dis[u][v]=w[u][v]。

  如果不相连则dis[u][v]=0x7fffffff

For (k = 1; k <= n; k++)

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

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

     If (dis[i][j] >dis[i][k] + dis[k][j])

         dis[i][j] = dis[i][k] + dis[k][j];

 算法结束:dis[i][j]得出的就是从i到j的最短路径。

3 总结

    经过一学期的ACM程序设计课程,我从费老师那里知道了山农现在ACM队的现状也不是太好。这学期有一次ACM程序设计省赛,但是我没去,经过半个学期的学习,我越发觉得自己的不足,所以觉得还是再学习一段时间,参加下一次的比赛吧,我相信学习ACM的同学都有一颗想要参加ACM程序设计大赛的心,ACM程序设计大赛是大学级别最高的脑力竞赛,素来被冠以"程序设计的奥林匹克"的尊称。大赛至今已有近40年的历史,是世界范围内历史最悠久、规模最大的程序设计竞赛。比赛形式是:从各大洲区域预赛出线的参赛队伍,于指定的时间、地点参加世界级的决赛,当然这需要极大的努力。在virtual judge上做题的时候感觉到题目的难度,有时候熬夜赶工只为了出那么一个题,老师所说的学这门课要牺牲大部分的业余时间大概就是这样吧,不过我感觉我还远远没有到达这种境界,还有些懒惰,做不来的题目,看别人做的要看很久才能明白。因此深深感到自己的不足,希望能参加暑假的集训来提高自己,只要这份热情还未泯灭,坚持,始终会在ACM的路上前行。

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值