ACM札记

1. 逗号表达式

  在“计蒜客“的ACM教程中,看到这样一段很好的代码:

int n;
while (scanf("%d", &n), n) {
  //do something       
}

  解释如下:

  scanf后面有个逗号,这就是所谓的逗号表达式。整个表达式的结果就是用逗号分隔的最后一个表达式的值。当n的值为0时,整个表达式的值就是0,此时退出循环。

 

2. ACM惯用头文件

  引用

#include <bits/stdc++.h>

  而无需引用两个头文件

#include <iostream>
#include <cstdio>

  这种方法并不是所有平台都适用的,用之前记得查阅平台的FAQ。不过在国内oj中,poj,hdu 不支持这个函数,这几个oj的编译器问题,其他国外的oj,还有台湾的oj都支持,CF(http://codeforces.com/),Topcoder也都支持。

 

3. 读取一整行字符串——scanf 与 gets 

  输入样本如下:

I promise I will no more cheat in tests.
2
1

  在这里

scanf("%s", str);

   是行不通的,因为文章——第一行的字符串——的内部会有空格,而这样的语句只能把空格之前的内容读入到str。

  因此我们要使用读取一整行的函数,如下:

gets(str);

  多提醒读者一句,使用gets的时候如果发生溢出的话程序是不会报错的,所以读者在使用gets的时候一定要小心哦!

 

4. 判断输入结束——scanf 函数

while (scanf("%d", &cnt) == 1) {
    //do something
}

  当scanf正确读入时,返回值是和读取的变量数目相等的。而当读取出错、读到文件末尾时,scanf函数会返回-1,因此当读到文件结束时,上述while循环就会中止。当然啦,也可以写成如下样式,效果是等价的。

while (scanf("%d", &cnt) != -1){
    //do something
}

 

5. int不够用时——long long类型

  输入数据的范围最大是10000,10000的立方是10^12,而int类型的最大值是2147483647,大约是2*10^9,远远超过了int的最大值,这样在计算过程中就会出现溢出的情况,导致结果错误。因此我们需要用更大的整数类型long long,它的最大值大约为10^19,即使10000个10^12也不会超过long long的取值范围。

  此外,记得long long输出是用%lld而非%d哦。

 

6. 关于Bug

  做题时不可避免会遇到程序错误(Bug),这时程序会在Online Judge上获得对应的错误信息,比如Wrong Answer, Runtime Error等。此时就需要各种debug方法来解决。通常debug有三类方法:通读程序;重读题目和使用数据调试。

  循环自增变量笔误是常见的bug,多见于多重循环的内层中。

 

7. 编程比赛

  Topcoder也是唯一一个必须下载客户端才能参加的程序设计竞赛。用户在Topcoder会有积分(rating)和积分对应的颜色标示,如上图中所 示,红>黄>蓝>绿>灰>白。积分最高的几个选手的id左侧会有一个类似靶心的标志,我们称之为target。 ACRush(楼天城)就是target选手之一。

  在 Linux上与mingw对应的是gcc和g++,由于这些都是系统自带的程序,在安装Codeblocks的时候不需要再单独安装一遍了。两个编译器在 一些细微之处还是有不同的,比如64位整型变量的输入(scanf)和输出(printf),在Linux下参数为%lld,而Windows下的参数 为%I64d。

  比如HDU(acm.hdu.edu.cn)和CF(codeforces.com)就是典型的Windows下搭建的OJ,不过大部分OJ还是Linux环境运行的。

  在提交用到64位整型的代码时一定要注意平台的操作系统,一般可以通过查看OJ的FAQ或在比赛现场咨询裁判获知。

  什么是代码能力?简单地说,如果你觉得学会了某一个算法或者数据结构,你能否在半个小时(或一小时)内完成程序,并且经过很少的调试一次通过?如果做不到 的话,只能说你还没彻底掌握这个算法或数据结构,要不断地训练,直到达到要求,只有这样才能保证在高度紧张的比赛中敢于实现而不犯错误。

 

8. 算法复杂度

  复杂度分为时间复杂度空间复杂度。通俗一点说,就是『跑多快』和『占多大内存』。

  在分析复杂度时,我们通常考虑它与什么成正比,并称之为算法的阶。比如程序执行了一个三重循环,每重循环执行n次,那么运行时间就和n4成正比。我们将与n4成正比写作O(n4)。再比如程序开辟了一个二维数组,下标范围是[0..n-1][0..n-1],那么程序的空间复杂度就是O(n2)。

  程序的运行时间不仅取决于时间复杂度,也会受计算复杂度、递归等因素的影响,但因此造成的差距最多也就是几倍。

  估算一个复杂度是否能承受目前的数据规模时,就将数值可能的最大值待遇复杂度的渐进式中,就能简单的判断算法是否能够满足运行时间限制的要求。比如O(n2)的算法,将n=1000代入得到1000000。通常来讲按照上述方法计算得到108基本是1s时限的最大值。

    - 当时间复杂度过高的时候,我们往往选择【空间换时间】,使用更高的空间复杂度来优化算法的时间复杂度。
    - 当数组长度为N时,二分查找的时间复杂度为O(logN),把时间复杂度中的一个N优化到lgN是很常用的策略。
    - 同一道题的不同解法,即使时间复杂度相同也有可能运行时间差出很多倍,因此对于代码细节的效率优化也同样重要,尤其是重复计算的化简。

 

9. 分治算法

  分治,即“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。总结起来就是:分解、解决、合并,这三大步。

  分治策略最经典的一个例子就是折半搜索算法,也就是二分查找——通过不断缩小解可能存在的范围,从而求得问题最优解的方法。二分查找是在有序数组中查找某一特定元素的搜索算法。搜索过程中从数组的中间元素开始,判断待查元素和中间元素的关系,来判断接下来是选择哪一半继续搜索。

  由于折半搜索算法每次都把搜索区域减少一半,所以时间复杂度是O(logN),要比朴素算法的O(N)高效许多,这也就是分治策略的优势所在:高效。

  分治策略的其他经典例子还有排序算法中的归并排序。归并排序的思路是将待排序的元素分成大致相同的两个子集合,分别对两个子集合进行排序,最终将排序的子集合合并成排好序的集合。

  分治所能解决的问题一般具有以下特征:

    -     问题的规模缩小到一定程度就可以轻松解决;
    -     问题可以分解为若干个规模较小的相同子问题;
    -     分解出的多个子问题的解可以合并为原问题的解。

  另外如果每个子问题之间不是独立的,那么就要重复地求解公共的子问题,此时更适合用我们后面会讲到的动态规划算法。

 

10. 穷举算法 \ 搜索算法

  穷举法,又称枚举法,是指从可能的解的集合中一一枚举各个元素,用给定的检验条件判定哪些是无用的,哪些是有用的。能使命题成立即为其解。

  穷举法本质上属于搜索算法,但它与搜索有所不同,因为适用于枚举法求解的问题必须满足:可预先确定解的数量;预先确定解变量的取值范围。

  穷举算法的关键,首先是确定循环的范围,其次是找出判断解的条件。将这两个关键点解决,穷举算法解题是轻而易举的。

  搜索算法和穷举法类似,对于那些无法用循环写出的穷举算法,使用递归或队列来实现求解。搜索的过程构成了一颗搜索树,搜索实际上是对搜索树的遍历。

  搜索的顺序分为两种:深度优先搜索(DFS)和广度优先搜索(BFS)。其中深度优先搜索是沿着搜索树的深度遍历树的节点,尽可能深地搜索树的分枝。DFS通常用递归的方法实现。

  广度优先搜索每次搜索其可以扩展的每一个节点,当一层节点全部搜索完成后,再依次搜索第一个可扩展节点可以扩展的所有节点。

  除了搜索的顺序之外,还有搜索的方法,一般分为盲目搜索和启发式搜索。

  启发式搜索是在搜索过程中加入了与问题有关的启发式信息,用于指导搜索朝着最有希望的方向前进,扫除不必要的搜索过程,加速问题求解并得到最优解。

  启发式搜索的常见算法有A*算法,模拟退火算法、遗传算法等,我们最常用的是A*算法。A*算法最经典案例是八数码问题,状态函数h设置为未到达目标位置的数字的个数。

 

11. 内存分区

  计算机中的内存在用于编程时,被人为的进行了分区(Segment),分为:

  -“栈区”(Stack)

  -“堆区”(Heap)

  -全局区(静态 区,Static)

  -文字常量区和程序代码区

  在前面的课程中,我们主要直接涉及到的是栈区的内存,在你的程序中,函数的参数值,局部变量的值等都被存在 了“栈区”,这部分的内存,是由系统来帮助你来管理的,没有特殊情况的时候,你是不需要对其进行特别处理的。计算机中内存的分配如下图。

  而针对堆区的内存,一般由程序员进行分配和释放, 使用堆内存的原因一般是

  -“栈上内存比较小,不够用”

  -“系统管理内存的方式死板,不方便用”

  对于堆上的内存,被程序员手动分配后,若程序员 不释放就可能会出现“内存泄漏”。很多企业级的应用,都因为内存泄漏而在“正常”运转很长时间后,轰然“坍塌”。在后面的入门课程中,我们会简单的对这块 的知识进行介绍。

  全局区、文字常量区和程序代码区在我们入门阶段,暂时还可以不去过多理解(甚至看不懂也无妨),只需要知道他们的大致作用即可——全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放;文字常量区是用于储存常量字符串的, 程序结束后由系统释放;程序代码区用于存放函数体的二进制代码。

 

12. 动态内存分配

  i, 申请单个对象

int *p;
p = new int;

  ii, 或者你还希望直接在申请的堆内存里直接包含值,比方说一个整数100,你可以写成

int *p;
p = new int(100);

  iii, 动态申请数组

int *p;
p = new int[n];

  这样对于一个已经申明的变量n可以申请长度为n的动态数组,就和前面课程学习过的一样;但是这里不能进行初始化。

  在这里,请注意一下new后类型后,一个值在圆括号和方括号里的区别。圆括号里值是用来赋初值的方括号里是用来说明申请堆空间的大小的

 

 参考文献  “计蒜客”  http://www.jisuanke.com/course

posted @ 2015-08-29 15:51  健康平安快乐  阅读(378)  评论(0编辑  收藏  举报