ACM札记

时间:2022-03-20 05:29:58

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) == ) {
//do something
}

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

while (scanf("%d", &cnt) != -){
//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)

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

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

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

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

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

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

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

12. 动态内存分配

  i, 申请单个对象

int *p;
p = new int;

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

int *p;
p = new int();

  iii, 动态申请数组

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

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

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

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