====== 常见编程技巧 ====== PS:除了内存池和常量数组,还搬了一些其他的小技巧。 ===== 内存池 ===== 当我们需要动态分配内存的时候,频繁使用 **new/malloc** 会占用大量的时间和空间,甚至生成大量的内存碎片从而降低程序的性能,可能会使原本正确的程序 **TLE/MLE**。 这时候我们就需要使用到「内存池」这种技巧:在真正使用内存之前,先申请分配一定大小的内存作为备用,当需要动态分配时则直接从备用内存中分配一块即可。 当然在大多数 **OI** 题当中,我们可以预先算出需要使用到的最大内存并一次性申请分配。 如申请动态分配**32**位有符号整数数组的代码: inline int* newarr(int sz) { static int pool[maxn], *allocp = pool; return allocp += sz, allocp - sz; } 线段树动态开点的代码: inline Node* newnode() { static Node pool[maxn << 1], *allocp = pool - 1; return ++allocp; } ===== 常量数组 ===== 善用常量数组往往能简化代码。定义常量数组时无须指明大小,编译器会计算。 下面是两道例题 $WERTYU$($UVa10082$) #include char s[] = "`1234567890-=QWERTYUIOP[]\\ASDFGHJKL;'ZXCVBNM,./"; int main(){ int i, c; while((c = getchar()) != EOF){ for (i=1; s[i] && s[i]!=c;i++); if(s[i]) putchar(s[i-1]); else putchar(c); } return 0; } 回文词($UVa401$) 输入一个字符串,判断它是否为回文串以及镜像串。输入字符串保证不含数字0。(使用常量数组) #include #include #include const char* rev = "A 3 HIL JM O 2TUVWXY51SE Z 8 "; const char* msg[] = {"not a palindrome", "a regular palindrome", "a mirrored string", "a mirrored palindrome"}; char r(char ch) { if(isalpha(ch)) return rev[ch - 'A']; return rev[ch - '0' + 25]; } int main() { char s[30]; while(scanf("%s", s) == 1) { int len = strlen(s); int p = 1, m = 1; for(int i = 0; i < (len+1)/2; i++) { if(s[i] != s[len-1-i]) p = 0; if(r(s[i]) != s[len-1-i]) m = 0; } printf("%s -- is %s.\n\n", s, msg[m*2+p]); } return 0; } ===== 对拍 ===== 有的时候我们写了一份代码,但是不知道它是不是正确的。这时候就可以用对拍的方法来进行检验或调试。 什么是对拍呢?具体而言,就是通过对比两个程序的输出来检验程序的正确性。你可以将自己程序的输出与其他程序(打的暴力或者其他 dalao 的标程)的输出进行对比,从而判断自己的程序是否正确。 当然,对拍过程要多次进行,我们需要通过批处理的方法来实现对拍的自动化。 具体而言,我们需要一个数据生成器,两个要进行对拍的程序。 每次运行一次 数据生成器 ,将生成的数据写入输入文件,通过重定向的方法使两个程序读入数据,并将输出写入指定文件,利用 **Windows** 下的 **fc** 命令比对文件(**Linux** 下为 **diff** 命令),从而检验程序的正确性。 如果发现程序出错,可以直接利用刚刚生成的数据进行调试啦。 对拍程序的大致框架如下: #include #include int main() { // For Windows //对拍时不开文件输入输出 //当然,这段程序也可以改写成批处理的形式 while (1) { system("gen > test.in"); //数据生成器将生成数据写入输入文件 system("test1.exe < test.in > a.out"); //获取程序1输出 system("test2.exe < test.in > b.out"); //获取程序2输出 if (system("fc a.out b.out")) { //该行语句比对输入输出 // fc返回0时表示输出一致,否则表示有不同处 system("pause"); //方便查看不同处 return 0; //该输入数据已经存放在test.in文件中,可以直接利用进行调试 } } } ===== 善用标识符进行调试 ===== 我们在本地测试的时候,往往要加入一些调试语句。要提交到 OJ 的时候,就要把他们全部删除,有些麻烦。 我们可以通过定义标识符的方式来进行本地调试。 大致的程序框架是这样的: #define DEBUG #ifdef DEBUG // do something #endif // or #ifndef DEBUG // do something #endif **#ifdef** 会检查程序中是否有通过 **#define** 定义的对应标识符,如果有定义,就会执行下面的内容, **#ifndef** 恰恰相反,会在没有定义相应标识符的情况下执行后面的语句。 我们提交程序的时候,只需要将 **#define DEBUG** 一行注释掉即可。 当然,我们也可以不在程序中定义标识符,而是通过 **-DDEBUG** 的编译选项在编译的时候定义 **DEBUG** 标识符。这样就可以在提交的时候不用修改程序了。 不少 **OJ** 都开启了 **-DONLINE_JUDGE** 这一编译选项,善用这一特性可以节约不少时间。 ===== 循环宏定义 ===== 我们写代码时,像下面这样的循环代码写得会非常多: for (int i = 0; i < N; i++) { } 为了简化这样的循环代码,我们可以使用宏定义: #define f(x, y, z) for (int x = (y), __ = (z); x < __; ++x) 这样写循环代码时,就可以简化成 **f(i, 0, N)** 。例如: // a is a STL container f(i, 0, a.size()) { ... } 另外推荐一个比较有用的宏定义: #define _rep(i, a, b) for (int i = (a); i <= (b); ++i) ===== 重载运算符 ===== 重载运算符是通过对运算符的重新定义,使得其支持特定数据类型的运算操作。 有的时候,我们构造了一种新的数据类型(高精度数,矩阵),当然可以将数据类型的运算操作用函数的形式写出来。但采用重载运算符的方式,可以让程序更为自然。 当然,重载运算符在语法上也有一些要求: 1. 重载运算符的一般形式为返回值类型 **operator** 运算符(参数,...) 2.在结构体内部重载运算符时,括号内的参数会少一个(事实上,另外一个参数是this指针,即指向当前参数的指针,也就是说,两元运算符只需要1个参数。(在结构体外部定义时,两元运算符还是需要2个参数) 其他要求就和普通函数的要求差不多了。 举一个简单的例子: #include struct pair_num//一个二元组类型 { int x,y; pair_num operator +(pair_num a)const //不加const会CE { pair_num res; res.x=x+a.x;//x事实上是this.x res.y=y+a.y; return res; } pair_num operator -(pair_num a)const { pair_num res; res.x=x-a.x; res.y=y-a.y; return res; } bool operator <(pair_num a)const //sort,set等数据结构需要使用小于号 { return x