ACM程序设计学期总结
Posted Heart.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM程序设计学期总结相关的知识,希望对你有一定的参考价值。
心路历程
大一的时候就跟着费老师学了近一个学期的ACM程序设计课程,但是由于种种出自于自己的原因,当时并没有完全塌下心去学,只是学到了些基础的东西,做的题太少,当然也并没有深入理解。大二又有了这个机会,自然不能放过,又认认真真重新学了一遍。
虽然这学期这门课一共只学了四个专题,但是对我来说,收获非常大,正像费老师说的:acm是思维的体操。它不停留在基本的语言层面,而是专注于解决各种实际问题的有一定难度的算法,它虽然会用到各种思想、各种工具,但是它不拘泥于某种固定的程式,而是需要无时不刻根据问题来变通,更要为了提高运行效率、降低时间复杂度而不断改进。 我相信这门课不仅对我以后的“程序猿”之路很有帮助,得到了新鲜雨露的逻辑思维对我的人生也必定有很大帮助。
ACM作为一个选修课,由于比较困难而且不好拿到学分,大多数同专业的同学都没有选,但我认为上大学去努力多拿学分和得一些暂时看着非常耀眼的奖项并不是多么重要的,重要的是能在这个过程中收获到什么,能够得到多少,能对自己的未来发展有多大的帮助。而这门课除了学分比较难拿以外,我觉得从各个方面对自己的提升和帮助都非常大,这也是我坚持下去努力去学的原因。本来还想着看有没有机会成为一个acm队员,但是费老师说如果是14级的准备考研的同学,原则上是不收的,因为时间已经不允许了,明年的秋天正是备战比赛的时候,而作为考研党那时也正是备战考研的关键时期。想当初,在大一的时候,如果一直坚持、努力,自己是有机会的,可是却被自己浪费掉了,再也没有后悔药。
无论如何,在人生的道路上,只能向前看,没有回头路,现在还是刚刚20岁的青年,还是清晨刚刚升起的太阳,只要从现在开始努力,一切都不算晚。对于自己的目标来说,自己的努力和付出还有很大的不足,不过令人高兴的事情还是有很多,自己一直在成长,一直在进步。下个学期还可以选费老师的acm后续课程,我会继续努力,希望能够可以更上一层楼。
在马上要走向尾声的这个学期里,对于这门课来说,学习它的大部分时间就是刷题,在做题之中理解相应的算法,实践自己的思想。大部分题都是自己独立完成,还有一部分自己实在ac不了也不去copy别人的代码,而是去看别人的解题思路,或者去理解别人代码中所蕴含的算法思想,自己理解透彻之后再自己去写,直到这道题通过。通过后,紧接着就去写博客,写清题目大意、解题思路、ac的代码以及自己的感想。费老师安排的这种方式非常有效,既督促了自己去刷题,又在写博客的过程中将自己解题所用到的思想在心中得到了很大的巩固,以后还可以去看自己的博客来实现高效率的复习,非常棒。
这个学期马上就要结束,第四个专题还在进行,自己的努力更会马不停蹄,坚持住自己的心,去朝着自己的目标不断迈进!
贪心算法
所谓贪心算法,就是在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
贪心算法的一般流程:
Greedy(A) //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;
}
我所认为的贪心算法并不是一个固定的程式,而是一种贪心思想,一种算法思想。虽然贪心算法是做出局部最优解来取得全局最优解,但是我认为要解决好一个问题首先要用全局的眼光看待和分析问题。就比如 ProblemF 1005 花最多数量的纸币 这个问题:给出1角、5角、1元、5元、10元的数量,以及想购买的图书价格,在不需要卖家找零的前提下,求出最少需要花多少张纸币以及最多需要花多少张纸币,如果不能实现则输出-1 -1。
要算出最少花多少纸币比较容易,但是如果直接正向的去求最多能花多少纸币则相当麻烦,但是如果反过来想,求最多花多少纸币就相当于最少剩多少纸币,这与前一相对简单的问题几乎一致,先去求它,就能将一个复杂的问题转化成了一个相对简单的问题。先用全局的眼光去看,只有这样才能清楚该怎么做最好,该怎么办,该怎么将大问题化解成小问题,也才知道该怎么在局部用贪心算法去解决整体的问题。
除了先用全局的眼光去看以外,还要学会用从数学的角度分析、解决问题。如:Problem D 1003 哈夫曼编码 这个问题:给出一组字符串,求出普通编码将占用的位数和哈夫曼编码所用的位数,以及普通编码与哈夫曼编码比率(普通编码长度除以哈夫曼编码长度)。
虽然算法上都是用哈夫曼树去解决问题,但是如果从数学的角度仔细分析、观察,会发现其中的数学规律:哈夫曼编码所占用的位数正好等于哈夫曼树各个非根节点的权值之和。因此,将字符储存好后按每个字母的数量将其进行排序,从小到大进行遍历相加进行哈夫曼树的组建,与此同时对哈夫曼树各个非根节点的权值进行累加即可得结果。而如果用这个规律去解决哈夫曼编码问题,问题将会迎刃而解,变得相当简单。
由于是用局部最优解获得全局最优解,在解决问题的过程中很容易产生边界问题,一些本必须被考虑、被处理的情况很有可能并没有被纳入贪心标准,也因此没有被处理,最终导致错误。所以一定要注意边界情况,不要少、漏。
此外,STL容器往往是伴随着贪心算法的一个重要帮手,利用STL容器可以在解决贪心问题的时候更加方便、快捷,效率也更高。因此在恰当的时候尽量多使用STL容器,既提高效率,也能提升对STL容器的熟练度。
典型题:一个两维的方格阵列,从左上角走到右下角,每个格子里都有一个数字K ( |K|<100 ),每一步(从(x,y)走)有三种走法:向下走一个格(x+1,y)、向右走一个格(x,y+1)或者(x,y*k) ,k为大于等于2的整数,走过的数字将累加。问:到达右下角时最大值为多少?
思路:先列遍历,再对每一行遍历,每一个格的最大值为当前格子原有的值加上所有的可能的上一步的最大值。注意格子里值的范围,有可能为负数。
实现代码:
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int c,n,m;
int dp[21][1001];
int cmax(int _i,int _j)
{
int _cmax=dp[_i][_j-1];
for(int i=2;i<=_j;++i)
if(_j%i==0)
if(dp[_i][_j/i]>_cmax)
_cmax=dp[_i][_j/i];
if(dp[_i-1][_j]>_cmax)
_cmax=dp[_i-1][_j];
return _cmax;
}
int main()
{
scanf("%d",&c);
while(c--)
{
scanf("%d%d",&n,&m);
for(inti=0;i<=m;++i)
dp[0][i]=-9999999;
for(inti=0;i<=n;++i)
dp[i][0]=-9999999;
for(inti=1;i<=n;++i)
for(intj=1;j<=m;++j){
scanf("%d",&dp[i][j]);
if(!(i==1&&j==1)) dp[i][j]+=cmax(i,j);
}
printf("%d\\n",dp[n][m]);
}
return 0;
}
搜索
所谓搜索算法,就是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案。
深搜(DFS)即深度优先搜索,始终对下一层的结点优先进行搜索,后面层数的结点遍历过后再返回上一层,逐层返回,直到根节点。深搜强调的是“全”,即所有结点都将至少遍历一遍。因此,深搜往往适用于解决一些“所有”类型的问题,如求:“所有方案”,“所有个数”。
广搜(BFS)即广度优先搜索,往往利用queue队列,始终优先遍历前一层的结点,当前一层的结点全部遍历过后再遍历下一层的结点。当发现所需目标时,立即结束搜索,不再进行下一步的遍历。因此,广搜往往适用于求“最”类型的问题,如求:“最少步数的”,“最小转折次数”。
搜索的实质是递归(尤其是深搜)、是穷举,遍历的过程中,所需求的状态必须满足一定的条件,最常见的如:不能超出地图范围,行走的方式等。
搜索实际上也是利用电脑的强大的计算能力进行大规模的运算求解答案,因此“剪枝”也非常的重要(对于oj的题来说,有时候不剪枝还会TLE),利用剪枝来减少计算量以及减少重复运算,从而降低时间复杂度。在剪枝时,常用的方法如:使用一个数组,来储存遍历过的位置、状态。
当掌握了搜索算法的思想、熟悉了DFS和BFS格式之后,解决问题的难点就在对问题的分析和条件的分析上了。要清楚如何利用搜索这个工具去解决这个问题,这关系着代码的具体实现。而当问题比较复杂的时候,需要满足的条件往往也不是那么显而易见,需要去思考、捉摸,考虑有可能出现的各个情况(符合要求的和不符合要求的条件都要考虑到),不能漏条件,更不能错。
典型题:给出一个地图,其中有一个起始点,标记为"."的地方可以走,为"#"的不能走。只能直走,不能斜向前进。求能到达的所有地区数。
思路:利用DFS,找出能到达的所有"."地区,每找到一个进行标记,从而剪枝、减少重复计算。
因此,此题递归共有3个条件:
①:所遍历到的行、列不能超出地图范围;
②:所遍历到的地区必须为"."标记;
③:此地区在之前的遍历过程中没有没标记过;
实现代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<queue>
using namespace std;
int cmap[21][21];
bool mark[21][21];
int dir[4][2]={{-1,0},{0,-1},{0,1},{1,0}};//上、左、右、下
int w,h;
int dfs(int r,int c)
{
int num=1;
for(int i=0;i<4;++i)
{
int temp_r=r;
int temp_c=c;
temp_r+=dir[i][0];
temp_c+=dir[i][1];
if(temp_r>=0&&temp_r<h&&temp_c>=0&&temp_c<w){
if(cmap[temp_r][temp_c]==2&&mark[temp_r][temp_c]==false){
mark[temp_r][temp_c]=true;
num+=dfs(temp_r,temp_c);
}
}
}
return num;
}
int main()
{
while(scanf("%d%d",&w,&h)!=EOF)
{
if(w==0)
return 0;
memset(mark,false,sizeof(mark));
int r_s,c_s;
for(int i=0;i<h;++i)
for(int j=0;j<w;++j){
char temp;
cin>>temp; //如果用scanf("%c",&temp);会出现错误,需要考虑输入中的回车问题。
if(temp=='@'){
cmap[i][j]=1;
r_s=i;
c_s=j;
}
else if(temp=='.')
cmap[i][j]=2;
else if(temp=='#')//此条件语句可不写。
cmap[i][j]=3;
}
mark[r_s][c_s]=true;
int cnt=dfs(r_s,c_s);
printf("%d\\n",cnt);
}
}
动态规划(DP)
所谓动态规划,就是先求取局部最优解,最后来得到全局最优解。或者是,先求得当前阶段的最优解,最后得到全部阶段结束后的最优解。
当求局部最优解时,也不能只是仅仅着眼于局部,而是考虑着全局,在符合全局的目标和条件下来求解局部最优解(这点有点像现实中的规划)。
既然重点是求局部,那么要弄清从哪里开始,到哪里结束。更要弄清开始时怎么设计,结束时和中间部分又是怎么设计,需不需要特殊的设定。
只有真正将对应的问题理解透彻,才能将对应的动态规划算法写好。
要弄清是用一个横向的结构来实现 如:蜜蜂爬蜂房,蜜蜂只能爬向右侧相邻的蜂房,计算蜜蜂从蜂房a爬到蜂房b的可能路线数。状态转移方程为:a[i]=a[i-1]+a[i-2]
还是树形结构来实现 如:数塔问题,给出一个数塔,从顶层走到底层,每一步只能走到相状态转移方程为:邻的结点,求经过的结点的最大数字之和。状态转移方程为:a[i][j]+=max(a[i+1][j],a[i+1][j+1]),
亦或矩阵结构来实现 如:求最长上升子序列的长度,给出X和Z两个字符串,求最长上升子序列的长度。利用矩阵。X字符串中的各字符依次作为行标,Z字符串中的各字符依次作为列标。从第一行第一列开始逐行遍历:如果当前位置对应的两个字符相同,则在这个位置记录"前一行前一列"的对应的数+1;如果当前位置对应的两个字符不同,则在这个位置记录"此行前一列"和"此列前一行"对应的两个数的最大值。遍历结束后,最后一行最后一列获得的数便是最长上升子序列的长度。
状态转移方程为:
if(a[i-1]==b[j-1])
cmap[i][j]=cmap[i-1][j-1]+1;
else
cmap[i][j]=cmax(cmap[i-1][j],cmap[i][j-1]);
这些结构都是具体实现算法的基础。
在做过的题中,有很多是背包问题,它们的结构都很相似,往往都是两层循环,外层对物品进行遍历,内层对背包的容量进行遍历。
虽然背包问题看着都很相似,但要想真正解决问题依然需要对问题有完全的认识和掌控,需要对细节滴水不漏的考量,也只有这样,才能在一些变化比较大的问题里游刃有余,不被固定的格式所限制,如:反向考虑的背包,某人准备抢银行,可以承受的最大被抓的概率为p(总共),共有n个银行可抢,分别给出各个银行所拥有的money:mi,抢各个银行被抓的概率pi。求可以抢到的最大金额。因为概率值的范围为0~1,即有小数,所以必须反向来考虑。
可以承受的最大被抓的概率为p,即:如果逃跑的概率大于1-p则符合要求。将所有银行的总钱数作为背包的容量,dp数组各元素对应的值为逃跑的概率。如果抢两个银行i和j,则逃跑概率为(1-pi)*(1-pj),即两个银行逃跑的概率之积。(涉及到概率论)状态转移方程为:dp[j]=max(dp[j],dp[j-a[i]]*(1-b[i]));
典型题:在一个无限大的平面,只能向前、向左、向右走,不能向后走,走过的路不能再走。给出走的步数n(n<=20),求总方案数。
思路:设F(n)为走n步的总方案数,a(n)为走n步最后一步为向前走的总数,b(n)为走n步最后一步为向左走或向右走的总数。
可以推出:
①F(n)=a(n)+b(n); (比较显而易见)
②a(n)=a(n-1)+b(n-1); (第n-1步不管是向前走的还是向左或向右走的都可以在第n步向前走)
③b(n)=2*a(n-1)+b(n-1);(第n-1步如果是向前走的,那么在第n步既可以向左走,也可以向右走,所以a(n-1)要乘以2;而第n-1步如果是向左走的,则不能向右走,第n-1步向右走的不能向左走,否则道路会塌陷,因此b(n-1)不用乘以2)
④a(n-1)=F(n-2); (不管第n-2步是如何走的,都可以在第n-1步向前走)
由上述4式可得状态转移方程式:a[i]=2*a[i-1]+a[i-2];
实现代码:
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
int a[21];
void dp()
{
a[0]=3;
a[1]=7;
for(int i=2;i<=19;++i){
a[i]=2*a[i-1]+a[i-2];
}
}
int main()
{
int n;
scanf("%d",&n);
dp();
while(n--)
{
int m;
scanf("%d",&m);
printf("%d\\n",a[m-1]);
}
return 0;
}
以上是关于ACM程序设计学期总结的主要内容,如果未能解决你的问题,请参考以下文章