ACM学期总结

来源:互联网 发布:php 去除数组重复值 编辑:程序博客网 时间:2024/05/18 01:23

ACM学期总结

大一上学期开始接触程序语言,作为一个零基础的学生,一开始对程序设计真可谓是一头雾水,因此,大一上学期并没有学到太多东西,只是跟着C语言老师学习了一下课本上的知识点,所以导致一开始接触ACM时遇到了不小的困难。

大一的寒假,我加了费老师建的冬季体验ACM兴趣群,开始学习ACM。我真的感到庆幸,当初下决心选修ACM,有不少的同学因为听说ACM有难度,不容易拿学分,因此放弃了ACM,但是我感觉我们不应该只是为了拿学分去选修一门课程,而是为了能够真正的学到东西,费老师也常对我们说,如果你是为拿学分而选修ACM,那么请你退出。正是因为选修了ACM,我的寒假第一次变得如此有意义,虽然一开始遇到了不小的困难(虽然实际上都是些很基本的东西),但是每当我解决一个问题都会很兴奋,我想这才应该是学习的初衷吧,用知识充实自己,才会让自己更加的快乐。

现在经过了一学期的学习,我进一步认识了ACM,认识了算法,深深地感到它的魅力,让我对效率二字深有感触,现在看来,学习ACM不仅仅是学习一些知识,更是培养一种习惯,让我们学会用理性的思维思考问题,提高自己的效率。这一个学期的学习过程中,我学到了与多东西,也深深地发现了自己的不足,而且我也发现了实践的重要性,有些知识虽然看书时感觉掌握了,但真正用的时候就不一定能够用好,相反在实践时还能够发现很多的问题。ACM是一个需要长久学习的东西,现在马上就要到暑假了,希望可以好好利用这个假期提升自己,也希望自己能够在ACM的道路上走的深一些。

不是每个人都天赋异禀,但是每个人都可以选择勤奋。

下面是对本学期所学知识的总结:

 

 

一.STL

 

 

STL是一个模版库,里面包含了很多实用的工具,是ACM入门内容。

1.)栈(stack)

 

stack是一种先进后出(First In Last Out, FILO)的数据结构,它只有一个出口,只能操作最顶端元素。

头文件: #include <stack>

定义:stack<data_type> stack_name;

         如:stack <int> s;

操作:

         empty() -- 返回bool型,表示栈内是否为空 (s.empty() )

         size() -- 返回栈内元素个数 (s.size() )

         top() -- 返回栈顶元素值 (s.top() )

         pop() -- 移除栈顶元素(s.pop(); )

         push(data_type a) -- 向栈压入一个元素 a(s.push(a); )

 

2.)队列(queue)

 

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

广度优先搜索算法中就使用了队列。

头文件: #include <queue>

定义:queue <data_type> queue_name;

         如:queue <int> q;

操作:

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

         size() -- 返回queue内元素个数 (q.size() )

         front() -- 返回queue内的下一个元素 (q.front() )

         back() -- 返回queue内的最后一个元素(q.back() )

         pop() -- 移除queue中的一个元素(q.pop(); )

         push(data_type a) -- 将一个元素a置入queue中(q.push(a); )

 

3.)优先队列(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() 返回优先队列是否为空

 

4.)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] )

 

5.)sort-快排

 

头文件: #include <algorithm>

Sort函数是一种快速排序函数,在程序中经常使用。

函数用法为

<1>sort(begin, end);

默认为在(begin,end)范围内升序排序。

<2>sort(begin, end, cmp);

先比上一个sort多了一个cmp参数,可以用来自己定义排列方式

如:定义

Intcmp(int a,int b)

{

   return(a>b);

}

这样就实现了在(begin,end)范围内降序排列。

 

6.)set 和 multiset

 

set 和 multiset会根据特定的排序准则,自动将元素排序,两者的不同之处在于multiset可以允许元素重复而set不允许元素重复。

头文件: #include <set>

定义:set <data_type> set_name;

         如:set <int> s;//默认由小到大排序

 

         如果想按照自己的方式排序,可以重载小于号。

         struct new_type{

                   int x, y;

                   bool operator < (constnew_type &a)const{

                            if(x != a.x) returnx < a.x;

                            return y < a.y;

                   }

         }

         set <new_type> s;

操作:

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的第一个元素位置。

s.upper_bound(elem)-- 返回元素值 > elem的第一个元素的位置。

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

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

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

 

7.)map和multimap

 

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

头文件: #include <map>

定义:map <data_type1, data_type2> map_name;

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

操作:

m.size()返回容器大小

m.empty()返回容器是否为空

m.count(key)返回键值等于key的元素的个数

m.lower_bound(key)返回键值等于key的元素的第一个可安插                的位置

m.upper_bound(key)返回键值等于key的元素的最后一个可安      插的位置

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

         a)运用value_type插入

                   map<string, float> m;

                   m.insert(map<string,float>:: value_type ("Robin", 22.3));

         b) 运用pair<>

                   m.insert(pair<string,float>("Robin", 22.3));

         c) 运用make_pair()

                   m.insert(make_pair("Robin",22.3));

 

 

二.递归递推

 

 

1.递归

递归是一种直接或间接调用自身的编程技巧,将大的问题转化为很多层相似的小问题,比较经典的就是n的阶乘。

递归的思想就是把一个不能或者不容易解决的大问题转化为一个或多个小问题,进一步转化为更小的问题,最小的问题可以直接解决,那么就能逐层的将大问题求出。

递归的关键是找到递归定义式以及递归的终止条件,

递归的定义必须使问题能够向边界条件转化,使问题越来越简单。

递归的终止条件是问题的最简单情况,终止条件本身不能再使用递归。

例: 欧几里德算法(辗转相除法)

gcd(m,n)=gcd(n,m%n)

 

intgcd(int a ,int b)

{

If(b==0)returna;

elsereturn gcd(b,a%b);

}

递归算法通过调用自身,是的程序的可读性比较高好,但是过多的调用函数产生了很多额外的开销,是的这种算法本身的时间复杂度就比较高,并且很多时候我们会重读性处理数据,造成了很多时间冗余,比如下例:

给定一个函数 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)。

如果只是简单的按照题目要求写出递归,就会超时,想一下,在递归过程中,有很多的过程都重复进行了很多次,因此,我们要采用记忆化的方法,用一个数组将所求的的值记录下来,下一次递归到该值的时候就可以直接输出了

递归部分为:

intfun(int a,int b,int c)

{

    int s;

    if(a<=0||b<=0||c<=0)return1;

    elseif(a>20||b>20||c>20)returnfun(20,20,20);

    else if(a<b&&b<c)

    {

        if(n[a][b][c]!=0)

            return n[a][b][c];

        else

        {

           s=fun(a,b,c-1)+fun(a,b-1,c-1)-fun(a,b-1,c);

            n[a][b][c]=s;

            return s;

        }

    }

    else

    {

        if(n[a][b][c]!=0)

            return n[a][b][c];

        else

        {

           s=fun(a-1,b,c)+fun(a-1,b-1,c)+fun(a-1,b,c-1)-fun(a-1,b-1,c-1);

            n[a][b][c]=s;

            return s;

        }

}

2.递推

递归问题就是一个问题的求解与前面的状态有所联系,所以如果找出前后过程的关系,就可以将复杂运算转换为若干步简单运算

递推的关键就是找到问题的前后关系并写出表达式。

很经典的一个问题就是斐波那契数列

F[0]=0; F[1]=1;F[N]=F[N-1]+F[N-2];(n>=2);

 

 

三.动态规划

 

 

动态规划是解决多阶段决策问题的一种方法,所谓多阶段决策问题,指的是如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。

多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.

最优性原理:

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

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

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

 

动态规划的基本模型:

1.问题具有多阶段决策的特征。

2.每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”。

3.每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态。

4.每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。

一般解题步骤:

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

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

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

4.找出边界条件。

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

6.递推求解。

 

动态规划问题的关键在于找到状态转移方程。动态规划其实就是用空间换时间。

例题:数塔问题

给定一个具有N层的数字三角形如下图,从顶至底有多条路径,每一步可沿左斜线向下或沿右斜线向下,路径所经过的数字之和为路径得分,请求出最大路径得分。

      7

     3  8

    8  1  0

    2  7 4  4

   4  5 2  6  5

int Max(int i,int j) //从当前位置开始的可得的最优值

{

  int s1,s2;                  //记录从左右斜线向下走的可达的最优值

  if(a[i][j]!=-1 returna[i][j];

  if (i>n) ||(j>0) return0 ;//当前位置不存在,最优值为-1

  s1=Max(i+1,j)+triangle[i,j];//沿左斜线向下走

 s2=Max(i+1,j+1)+triangle[i,j]; //沿右斜线向下走

  if s1>s2 return s1

  else return s2;

}

 

在动态规划问题中,通常的(二维)动态规划问题往往要申请一个大小为n,m的二维数组,然后采用for循环来对每一阶段进行,这样的话空间消耗为(n*m)是比较大的,而对于n*m过大的问题,申请数组时往往会因过大而出错,这时候,就需要采用动态规划中的一个常见处理方法----"滚动数组",对于大部分动态规划问题可以大大降低空间消耗,具体方法用一个例子来说明

菲波那切数列问题:

状态转移方程为:dp [i]=dp[i-1]+dp[i-2]

在这个转移方程中,我们用到了 i, i-1,i-2,一共三个阶段的值,其实对于所有的阶段,都只需要三个阶段的值,因此我们可以这么写

dp[i%3]=dp[(i-1)%3]+dp[(i-2)%3]              

这就是滚动数组的取余

这样的话,如果要求第n(n很大)个数的值,我们并不需要定义长度为n的数组了,而只用定义数组dp[3],就可以实现了,这样就大大降低了空间的使用,这种如同滚动使用dp空间的方法就是滚动数组,当然,使用滚动数组也只是节省空间而已,对于时间并没有什么影响

 

 

四.背包问题

 

 

问题的主要框架为

有N种物品,以及一个容量为V的背包,每种物品的费用(所占用的体积)为c[ i ],价值为w[ i ],求解如何装物品可以达到要求,根据每种物品的数量不同,背包问题可以分为 01背包,多重背包 以及 完全背包这三类背包问题。

 

1.当所有种类的物品只有一件时就是01背包问题了,这是背包问题中最基本的一类问题了。因此这类问题的思路就是对于每种物品选择放或是不放

用f[ i ][ v ]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

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

因为这个状态转移方程只涉及到i和i-1的值,因此我们可以写出一个伪码:

for i=1..N

                            forv=V..0

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

注意内层循环从大到小,避免一件物品多次选择。

 

2.当所有种类的物品有无限多时,问题就变为了完全背包

简单优化:

1)若两件物品i、j满足c[i]<=c[j]    并且w[i]>=w[j],则将物品j去掉,不用考虑 .

2)将费用大于V的物品去掉 .

转化为01背包:

考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题

类似于01背包,完全背包的伪码:

for i=1..N

                            forv=0..V

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

但是要注意的是内层循环01背包和完全背包是不同的。

 

3.当每种物品有n[i](大于一但不是无限)个数时,问题变为多重背包问题,

 

 

#include<stdio.h>

#include<string.h>

#include<stdlib.h>

#define N 1000         //物品个数

#define M 100000000    //所有物品可能的最大价值

int m[N],c[N],w[N],f[M],V;

void ZeroOnePack(int cost,intweight)(01背包)

{

   int v;

   for(v=V;v>=cost;v--)

       f[v]=max(f[v],f[v-cost]+weight);

}

void CompletePack(int cost,int weight)(完全背包)

{

   int v;

   for(v=cost;v<=V;v++)

       f[v]=max(f[v],f[v-cost]+weight);

}

void MultiplePack(int cost,intweight,int amount)(多重背包)

{

   int k;

   if(cost*amount>=V)

   {

       CompletePack(cost,weight);

       return;

   }

   k=1;

   while(k<amount)

   {

       ZeroOnePack(k*cost,k*weight);

       amount=amount-k;

       k=k*2;

   }

   ZeroOnePack(amount*cost,amount*weight);

}

int main()

{

   int n,i;

   scanf("%d %d",&n,&V);

                                                  // 两种不同的初始化方式,根据情况自行选择

   //memset(f,0,sizeof(f[0])*(V+1));              // 只希望价格尽量大

   //memset(f,-M,sizeof(f[0])*(V+1));

f[0]=0;      // 要求恰好装满背包

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

scanf("%d %d%d",m+i,c+i,w+i);

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

MultiplePack(c[i],w[i],m[i]);

   printf("%d\n",f[V]);

   return 0;

}

 

这是一个多重背包的模板,也是十分好用的一种模板,因为这个比直接拆除01 背包来做要省些时间。这是为啥呢,首先讲一下为什么能换成01 背包吧。

假如给了我们价值为 2,但是数量却是10 的物品,我们应该把10给拆开,要知道二进制可是能够表示任何数的,所以10 就是可以有1,2, 4,8之内的数把它组成,一开始我们选上 1了,然后让10-1=9,再选上2,9-2=7,在选上 4,7-4=3,

而这时的3<8了,所以我们就是可以得出 10由 1,2,4,3,来组成,就是这个数量为1,2,3,4的物品了,那么他们的价值是什么呢,是2,4,6,8,也就说给我们的价值为2,数量是10的这批货物,已经转化成了价值分别是2,4,6,8元的货物了,每种只有一件,这就是二进制优化的思想。

那为什么会有完全背包和01 背包的不同使用加判断呢?原因也很简单,当数据很大,大于背包的容纳量时,我们就是在这个物品中取上几件就是了,取得量时不知道的,也就理解为无限的了,这就是完全背包,反而小于容纳量的就是转化为01背包来处理就是了,可以大量的省时间。

 

 

五.二分查找

 

 

简单定义:在一个单调有序的集合中查找元素,每次将集合分为左右两部分,判断解在哪个部分中并调整集合上下界,重复直到找到目标元素。

与中学数学所学的二分法是一样的。

二分查找必须注意的一点是所查找的这个序列必须是有序的,如果无序这无法实现。因此对于一些序列,在不改变题意的情况下可以先进行排序再用二分。

double low=“区间下界”,high=“区间上界”,mid;

while(通过high与low的条件关系控制循环)

{

          mid = (high + low)/2;

    if(Caculate(mid)<x)

        low=mid;

    else

        high=mid;

}

这是基本的模版,但是while中的限制条件是要根据集体问题写的,这个条件比较关键,具体问题具体分析,现在条件不当有时会导致问题出错。

 

二分法延伸——三分

 

三分问题与二分问题的思想是一样的通过缩小范围来找出结果值,不同的是二分法所解决的是单调有序问题,而三分法解决的是类似于抛物线那种的先增后减或先减后增的问题。

 

 

 

类似二分的定义Left和Right

mid = (Left + Right) / 2

midmid = (mid + Right) / 2;

如果mid靠近极值点,则Right = midmid;

否则(即midmid靠近极值点),则Left = mid;

 

 

六.贪心算法

 

 

贪心算法与动态规划问题比较类似,都是分阶段的问题,求的也都是最优解,但是贪心算法所关注的是当前阶段的最优解,贪心算法的思想就是对于当前阶段选取最优的解,最后将每一阶段最优解合在一起,因此贪心算法的弊端也很明显——目光短浅,但是如果有题目自身决定了仅由每一阶段的最优解就可以得到整体最优解的话,贪心算法是诸多算法中最好的一个。

贪心算法使用之前比较关键的一步就是判断是否能够通过每一阶段的最优解得到整体最优解,也就是要判断使用贪心算法能否得到正确的解。

 

一般流程:

//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;

(1)候选集合A:问题的最终解均取自于候选集合A。

(2)解集合S:解集合S不断扩展,直到构成满足问题的完整解。

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

(4)选择函数select:贪心策略,这是贪心算法的关键。

(5)可行函数feasible:解集合扩展后是否满足约束条件。

 

 

七.搜索

 

 

搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或则所有的可能情况,从而求出问题的解,相对于单纯的枚举算法有了一定的方向性和目的性。算法在解的空间里从一种状态按照要求转移到其他状态,一直这样进行下去将解的空间中的状态遍历,找到目标状态。

搜索按照方式不同分为深度优先搜索(DFS)和广度优先搜索(BFS)

 

广度优先搜索利用队列,按照层数一层一层的来遍历,将一层全部扩展,然后再进行下一层。恰好利用了队列的先进先出的性质。

具体的过程为:

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

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

3 将初始状态删除

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

广度优先搜索框架:

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

 

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

具体的过程为:

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

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

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

深度优先搜索框架:

1)递归实现:

FunctionDfs (Int Step, 当前状态)

Begin

可加结束条件

从当前状态循环拓展下一个状态Next

If 状态Next合法 Then

Dfs (Step + 1, Next ))

End

2)非递归实现:

WhileNot Stack.Empty ()

Begin

Tmp = Stack.top()

从Tmp拓展下一个未拓展的状态Next

If 没有未拓展状态(到达叶节点) Then

Stack.pop()

else If 状态Next合法 Then

Stack.push(Next)

End

 

原创粉丝点击