oiwiki技巧

读入、输出优化

在默认情况下,std::cin/std::cout 是极为迟缓的读入/输出方式,而 scanf/printf 比 std::cin/std::cout 快得多。

注意

cin/cout 与 scanf/printf 的实际速度差会随编译器和操作系统的不同发生一定的改变。如果想要进行详细对比,请以实际测试结果为准。

下文将详细介绍读入输出的优化方法。

关闭同步/解除绑定

std::ios::sync_with_stdio(false)

这个函数是一个“是否兼容 stdio”的开关,C++ 为了兼容 C,保证程序在使用了 printf 和 std::cout 的时候不发生混乱,将输出流绑到了一起。同步的输出流是线程安全的。

这其实是 C++ 为了兼容而采取的保守措施,也是使 cin/cout 速度较慢的主要原因。我们可以在进行 IO 操作之前将 stdio 解除绑定,但是在这样做之后要注意不能同时使用 std::cin 和 scanf,也不能同时使用 std::cout 和 printf,但是可以同时使用 std::cin 和 printf,也可以同时使用 scanf 和 std::cout

tie

tie 是将两个 stream 绑定的函数,空参数的话返回当前的输出流指针。

在默认的情况下 std::cin 绑定的是 std::cout,每次执行 << 操作符的时候都要调用 flush() 来清理 stream buffer,这样会增加 IO 负担。可以通过 std::cin.tie(0)(0 表示 NULL)来解除 std::cin 与 std::cout 的绑定,进一步加快执行效率。

但需要注意的是,在解除了 std::cin 和 std::cout 的绑定后,程序中必须手动 flush 才能确保每次 std::cout 展现的内容可以在 std::cin 前出现。这是因为 std::cout 被 buffer 为默认设置。例如:

1
2
3
4
5
6
std::cout
    << "Please input your name: "
    << std::flush;  // 或者: std::endl;
                    // 因为每次调用std::endl都会flush输出缓冲区,而 \n 则不会。
// 但请谨慎使用,过多的flush会影响程序效率
std::cin >> name;

代码实现

1
2
3
std::ios::sync_with_stdio(false);
std::cin.tie(0);
// 如果编译开启了 C++11 或更高版本,建议使用 std::cin.tie(nullptr);

读入优化

scanf 和 printf 依然有优化的空间,这就是本章所介绍的内容——读入和输出优化。

  • 注意,本页面中介绍的读入和输出优化均针对整型数据,若要支持其他类型的数据(如浮点数),可自行按照本页面介绍的优化原理来编写代码。

原理

众所周知,getchar 是用来读入 1 byte 的数据并将其转换为 char 类型的函数,且速度很快,故可以用“读入字符——转换为整型”来代替缓慢的读入

每个整数由两部分组成——符号和数字

整数的 '+' 通常是省略的,且不会对后面数字所代表的值产生影响,而 '-' 不可省略,因此要进行判定

10 进制整数中是不含空格或除 0~9 和正负号外的其他字符的,因此在读入不应存在于整数中的字符(通常为空格)时,就可以判定已经读入结束

C 和 C++ 语言分别在 ctype.h 和 cctype 头文件中,提供了函数 isdigit, 这个函数会检查传入的参数是否为十进制数字字符,是则返回 true,否则返回 false。对应的,在下面的代码中,可以使用 isdigit(ch) 代替 ch >= '0' && ch <= '9',而可以使用 !isdigit(ch) 代替 ch <'0' || ch> '9'

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int read() {
  int x = 0, w = 1;
  char ch = 0;
  while (ch < '0' || ch > '9') {  // ch 不是数字时
    if (ch == '-') w = -1;        // 判断是否为负
    ch = getchar();               // 继续读入
  }
  while (ch >= '0' && ch <= '9') {  // ch 是数字时
    x = x * 10 + (ch - '0');  // 将新读入的数字’加’在 x 的后面
    // x 是 int 类型,char 类型的 ch 和 ’0’ 会被自动转为其对应的
    // ASCII 码,相当于将 ch 转化为对应数字
    // 此处也可以使用 (x<<3)+(x<<1) 的写法来代替 x*10
    ch = getchar();  // 继续读入
  }
  return x * w;  // 数字 * 正负号 = 实际数值
}
  • 举例

读入 num 可写为 num=read();

输出优化

原理

同样是众所周知,putchar 是用来输出单个字符的函数

因此将数字的每一位转化为字符输出以加速

要注意的是,负号要单独判断输出,并且每次 %(mod)取出的是数字末位,因此要倒序输出

代码实现

1
2
3
4
5
6
7
8
void write(int x) {
  if (x < 0) {  // 判负 + 输出负号 + 变原数为正数
    x = -x;
    putchar('-');
  }
  if (x > 9) write(x / 10);  // 递归,将除最后一位外的其他部分放到递归中输出
  putchar(x % 10 + '0');  // 已经输出(递归)完 x 末位前的所有数字,输出末位
}

但是递归实现常数是较大的,我们可以写一个栈来实现这个过程

1
2
3
4
5
6
7
8
inline void write(int x) {
  static int sta[35];
  int top = 0;
  do {
    sta[top++] = x % 10, x /= 10;
  } while (x);
  while (top) putchar(sta[--top] + 48);  // 48 是 '0'
}
  • 举例

输出 num 可写为 write(num);

更快的读入/输出优化

通过 fread 或者 mmap 可以实现更快的读入。

fread 能将需要的文件部分读入内存缓冲区。mmap 则会调度内核级函数,将文件一次性地映射到内存中,类似于可以指针引用的内存区域。所以在日常程序读写时,只需要重复读取部分文件可以使用 fread,因为如果用 mmap 反复读取一小块文件,做一次性内存映射并且内核处理 page fault 的花费会远比使用 fread 的内核级函数调度大。

一次性读入缓冲区的操作比逐个字符读入(getchar,putchar)要快的多。因为硬盘的多次读写速度是要慢于直接读取内存的,所以先一次性读到缓存区里再从缓存区读入要快的多。并且 mmap 确保了进程间自动共享,存储区如果可以也会与内核缓存分享信息,确保了更少的拷贝操作。

更通用的是 fread,因为 mmap 不能在 Windows 环境下使用(例如 CodeForces 的 tester)。

fread 类似于参数为 "%s" 的 scanf,不过它更为快速,而且可以一次性读入若干个字符(包括空格换行等制表符),如果缓存区足够大,甚至可以一次性读入整个文件。

对于输出,我们还有对应的 fwrite 函数

1
2
3
4
std::size_t fread(void* buffer, std::size_t size, std::size_t count,
                  std::FILE* stream);
std::size_t fwrite(const void* buffer, std::size_t size, std::size_t count,
                   std::FILE* stream);

使用示例:fread(Buf, 1, SIZE, stdin),表示从 stdin 文件流中读入 SIZE 个大小为 1 byte 的数据块到 Buf 中。

读入之后的使用就跟普通的读入优化相似了,只需要重定义一下 getchar。它原来是从文件中读入一个 char,现在变成从 Buf 中读入一个 char,也就是头指针向后移动一位。

1
2
3
4
5
char buf[1 << 20], *p1, *p2;
#define gc()                                                               \
  (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) \
       ? EOF                                                               \
       : *p1++)

fwrite 也是类似的,先放入一个 OutBuf[MAXSIZE] 中,最后通过 fwrite 一次性将 OutBuf 输出。

参考代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace IO {
const int MAXSIZE = 1 << 20;
char buf[MAXSIZE], *p1, *p2;
#define gc()                                                               \
  (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, MAXSIZE, stdin), p1 == p2) \
       ? EOF                                                               \
       : *p1++)

inline int rd() {
  int x = 0, f = 1;
  char c = gc();
  while (!isdigit(c)) {
    if (c == '-') f = -1;
    c = gc();
  }
  while (isdigit(c)) x = x * 10 + (c ^ 48), c = gc();
  return x * f;
}

char pbuf[1 << 20], *pp = pbuf;

inline void push(const char &c) {
  if (pp - pbuf == 1 << 20) fwrite(pbuf, 1, 1 << 20, stdout), pp = pbuf;
  *pp++ = c;
}

inline void write(int x) {
  static int sta[35];
  int top = 0;
  do {
    sta[top++] = x % 10, x /= 10;
  } while (x);
  while (top) push(sta[--top] + '0');
}
}  // namespace IO

输入输出的缓冲

printf 和 scanf 是有缓冲区的。这也就是为什么,如果输入函数紧跟在输出函数之后/输出函数紧跟在输入函数之后可能导致错误。

刷新缓冲区

  1. 程序结束
  2. 关闭文件
  3. printf 输出 \r 或者 \n 到终端的时候(注:如果是输出到文件,则不会刷新缓冲区)
  4. 手动 fflush()
  5. 缓冲区满自动刷新
  6. cout 输出 endl

使输入输出优化更为通用

如果你的程序使用多个类型的变量,那么可能需要写多个输入输出优化的函数。下面给出的代码使用 C++ 中的 template 实现了对于所有整数类型的输入输出优化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename T>
inline T
read() {  // 声明 template 类,要求提供输入的类型T,并以此类型定义内联函数 read()
  T sum = 0, fl = 1;  // 将 sum,fl 和 ch 以输入的类型定义
  int ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') fl = -1;
  for (; isdigit(ch); ch = getchar()) sum = sum * 10 + ch - '0';
  return sum * fl;
}

如果要分别输入 int 类型的变量 a,long long 类型的变量 b 和 __int128 类型的变量 c,那么可以写成

1
2
3
a = read<int>();
b = read<long long>();
c = read<__int128>();

完整带调试版

关闭调试开关时使用 fread(),fwrite(),退出时自动析构执行 fwrite()

开启调试开关时使用 getchar(),putchar(),便于调试。

若要开启文件读写时,请在所有读写之前加入 freopen()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// #define DEBUG 1  // 调试开关
struct IO {
#define MAXSIZE (1 << 20)
#define isdigit(x) (x >= '0' && x <= '9')
  char buf[MAXSIZE], *p1, *p2;
  char pbuf[MAXSIZE], *pp;
#if DEBUG
#else
  IO() : p1(buf), p2(buf), pp(pbuf) {}

  ~IO() { fwrite(pbuf, 1, pp - pbuf, stdout); }
#endif
  inline char gc() {
#if DEBUG  // 调试,可显示字符
    return getchar();
#endif
    if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, MAXSIZE, stdin);
    return p1 == p2 ? ' ' : *p1++;
  }

  inline bool blank(char ch) {
    return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
  }

  template <class T>
  inline void read(T &x) {
    register double tmp = 1;
    register bool sign = 0;
    x = 0;
    register char ch = gc();
    for (; !isdigit(ch); ch = gc())
      if (ch == '-') sign = 1;
    for (; isdigit(ch); ch = gc()) x = x * 10 + (ch - '0');
    if (ch == '.')
      for (ch = gc(); isdigit(ch); ch = gc())
        tmp /= 10.0, x += tmp * (ch - '0');
    if (sign) x = -x;
  }

  inline void read(char *s) {
    register char ch = gc();
    for (; blank(ch); ch = gc())
      ;
    for (; !blank(ch); ch = gc()) *s++ = ch;
    *s = 0;
  }

  inline void read(char &c) {
    for (c = gc(); blank(c); c = gc())
      ;
  }

  inline void push(const char &c) {
#if DEBUG  // 调试,可显示字符
    putchar(c);
#else
    if (pp - pbuf == MAXSIZE) fwrite(pbuf, 1, MAXSIZE, stdout), pp = pbuf;
    *pp++ = c;
#endif
  }

  template <class T>
  inline void write(T x) {
    if (x < 0) x = -x, push('-');  // 负数输出
    static T sta[35];
    T top = 0;
    do {
      sta[top++] = x % 10, x /= 10;
    } while (x);
    while (top) push(sta[--top] + '0');
  }

  template <class T>
  inline void write(T x, char lastChar) {
    write(x), push(lastChar);
  }
} io;

 

 

常见错误

本页面主要列举一些竞赛中很多人经常会出现的错误。

会引起 CE 的错误

这类错误多为词法、语法和语义错误,引发的原因较为简单,修复难度较低。

例:

  • int main() 写为 int mian() 之类的拼写错误。

  • 写完 struct 或 class 忘记写分号。

  • 数组开太大,(在 OJ 上)使用了不合法的函数(例如多线程),或者函数声明但未定义,会引起链接错误。

  • 函数参数类型不匹配。

    • 示例:如使用 <algorithm> 头文件中的 max 函数时,传入了一个 int 类型参数和一个 long long 类型参数。

      1
      2
      3
      4
      // query 为返回 long long 类型的自定义函数
      printf("%lld\n", max(0, query(1, 1, n, l, r));
      
      //错误    没有与参数列表匹配的 重载函数 "std::max" 实例
  • 使用 goto 和 switch-case 的时候跳过了一些局部变量的初始化。

不会引起 CE 但会引起 Warning 的错误

犯这类错误时写下的程序虽然能通过编译,但大概率会得到错误的程序运行结果。这类错误会在使用 -W{warningtype} 参数编译时被编译器指出。

  • 赋值运算符 = 和比较运算符 == 不分。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      std::srand(std::time(nullptr));
      int n = std::rand();
      if (n = 1)
        printf("Yes");
      else
        printf("No");
      
      // 无论 n 的随机所得值为多少,输出肯定是 Yes
      // 警告    运算符不正确: 在 Boolean 上下文中执行了常量赋值。应考虑改用“==”。
    • 如果确实想在原应使用 == 的语句里使用 =(比如 while (foo = bar)),又不想收到 Warning,可以使用 双括号while ((foo = bar))

  • 由于运算符优先级产生的错误。

    • 示例:

      1
      2
      3
      4
      5
      6
      // 错误
      // std::cout << (1 << 1 + 1);
      // 正确
      std::cout << ((1 << 1) + 1);
      
      // 警告    “<<”: 检查运算符优先级是否有可能的错误;使用括号阐明优先级
  • 不正确地使用 static 修饰符。

  • 使用 scanf 读入的时候没加取地址符 &

  • 使用 scanf 或 printf 的时候参数类型与格式指定符不符。

  • 同时使用位运算和逻辑运算符 == 并且未加括号。

    • 示例:(x >> j) & 3 == 2
  • int 字面量溢出。

    • 示例:long long x = 0x7f7f7f7f7f7f7f7f1<<62
  • 未初始化局部变量,导致局部变量被赋予垃圾初值。

    未初始化变量会发生什么

    原文:https://loj.ac/d/3679 by @hly1204

    例如我们在 C++ 中声明一个 int a; 但不初始化,可能有时候会认为 a 是一个“随机”(其实可能不是真的随机)的值,但是可能将其认为是一个固定的值,但实际上并非如此。

    我们在简单的测试代码中

    https://wandbox.org/permlink/T2uiVe4n9Hg4EyWT

    代码是:

    1
    2
    3
    4
    5
    6
    7
    #include <iostream>
    
    int main() {
      int a;
      std::cout << std::boolalpha << (a < 0 || a == 0 || a > 0);
      return 0;
    }

    在一些编译器和环境上开启优化后,其输出为 false。

    有兴趣的话可以看 https://www.ralfj.de/blog/2019/07/14/uninit.html,尽管其实用 Rust 做的实验,但是本质是一样的。

  • 局部变量与全局变量重名,导致全局变量被意外覆盖。(开 -Wshadow 就可检查此类错误。)

  • 运算符重载后引发的输出错误。

    • 示例:
      1
      2
      3
      4
      // 本意:前一个 << 为重载后的运算符,表示输出;后一个 << 为移位运算符,表示将 1
      // 左移 1 位。 但由于忘记加括号,导致编译器将后一个 <<
      // 也判作输出运算符,而导致输出的结果与预期不同。 错误 std::cout << 1 << 1; 正确
      std::cout << (1 << 1);

既不会引起 CE 也不会引发 Warning 的错误

这类错误无法被编译器发现,仅能自行查明。

会导致 WA 的错误

  • 上一组数据处理完毕,读入下一组数据前,未清空数组。

  • 读入优化未判断负数。

  • 所用数据类型位宽不足,导致溢出。

    • 如习语“三年 OI 一场空,不开 long long 见祖宗”所描述的场景。选手因为没有在正确的地方开 long long(将整数定义为 long long 类型),导致得出错误的答案而失分。
  • 存图时,节点编号 0 开始,而题目给的边中两个端点的编号从 1 开始,读入的时候忘记 -1。

  • 大/小于号打错或打反。

  • 在执行 ios::sync_with_stdio(false); 后混用 scanf/printf 和 std::cin/std::cout 两种 IO,导致输入/输出错乱。

    • 示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      // 这个例子将说明关闭与 stdio 的同步后,混用两种 IO 方式的后果
      // 建议单步运行来观察效果
      #include <cstdio>
      #include <iostream>
      
      int main() {
        // 关闭同步后,cin/cout 将使用独立缓冲区,而不是将输出同步至 scanf/printf
        // 的缓冲区,从而减少 IO 耗时
        std::ios::sync_with_stdio(false);
        // cout 下,使用'\n'换行时,内容会被缓冲而不会被立刻输出
        std::cout << "a\n";
        // printf 的 '\n' 会刷新 printf 的缓冲区,导致输出错位
        printf("b\n");
        std::cout << "c\n";
        // 程序结束时,cout 的缓冲区才会被输出
        return 0;
      }
    • 特别的,也不能在执行 ios::sync_with_stdio(false); 后使用 freopen

  • 由于宏的展开,且未加括号导致的错误。

    • 示例:该宏返回的值并非  而是 。

      1
      2
      #define square(x) x* x
      printf("%d", square(2 + 2));
  • 哈希的时候没有使用 unsigned 导致的运算错误。

    • 对负数的右移运算会在最高位补 1。参见:位运算
  • 没有删除或注释掉调试输出语句。

  • 误加了 ;

    • 示例:

      1
      2
      3
      /* clang-format off */
      while (1);
          printf("OI Wiki!\n");
  • 哨兵值设置错误。例如,平衡树的 0 节点。

  • 在类或结构体的构造函数中使用 : 初始化变量时,变量声明顺序不符合初始化时候的依赖关系。

    • 成员变量的初始化顺序与它们在类中声明的顺序有关,而与初始化列表中的顺序无关。参见:构造函数与成员初始化器列表 的“初始化顺序”
    • 示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      #include <iostream>
      
      class Foo {
       public:
        int a, b;
      
        // a 将在 b 前初始化,其值不确定
        Foo(int x) : b(x), a(b + 1) {}
      };
      
      int main() {
        Foo bar(1, 2);
        std::cout << bar.a << ' ' << bar.b;
      }
      
      // 可能的输出结果:-858993459 1
  • 并查集合并集合时没有把两个元素的祖先合并。

    • 示例:

      1
      2
      f[a] = b;              // 错误
      f[find(a)] = find(b);  // 正确

换行符不同

Warning

在正式比赛中会尽量保证选手答题的环境和最终测试的环境相同。

本节内容仅适用于模拟赛等情况,而我们也建议出题人尽量让数据符合 数据格式

不同的操作系统使用不同的符号来标记换行,以下为几种常用系统的换行符:

  • LF(用 \n 表示):Unix 或 Unix 兼容系统

  • CR+LF(用 \r\n 表示):Windows

  • CR(用 \r 表示):Mac OS 至版本 9

而 C/C++ 利用转义序列 \n 来换行,这可能会导致我们认为输入中的换行符也一定是由 \n 来表示,而只读入了一个字符来代表换行符,这就会导致我们没有完全读入输入文件。

以下为解决方案:

  • 多次 getchar(),直到读到想要的字符为止。

  • 使用 cin 读入,这可能会增大代码常数

  • 使用 scanf("%s",str) 读入一个字符串,然后取 str[0] 作为读入的字符。

  • 使用 scanf(" %c",&c) 过滤掉所有空白字符。

会导致未知的结果

未定义行为会导致未知的结果,可能是 WA,RE 等。

  • 除以 0(求 0 的逆元)

    示例
    1
    cout << x / 0 << endl;
  • 数组(下标)越界

    例如:

    • 未正确设置循环的初值导致访问了下标为 -1 的值。

    • 无向图边表未开 2 倍。

    • 线段树未开 4 倍空间。

    • 看错数据范围,少打一个零。

    • 错误预估了算法的空间复杂度。

    • 写线段树的时候,pushup 或 pushdown 叶节点。

      正确的做法:不要越界,记得检查自己的代码,使得下标访问数 x,在定义的下标中。

  • 除 main 外有返回值函数执行至结尾未执行任何 return 语句

    即使有一个分支有返回值,但是其他分支却没有,结果也是未定义的。

    可以开 -Wall 选项,看一看自己有没有关于函数未 return 的警告。

  • 尝试修改字符串字面量

    示例
    1
    2
    3
    char *p = "OI-wiki";
    p[0] = 'o';
    p[1] = 'i';

    这样定义并不符合 c++11 标准,迎丹使用其他 合适 的数据类型,例如 std::string,char[]

  • 多次释放/非法解引用一片内存

    例如:

    • 未初始化就解引用指针。

    • 指针指向的内存区域已经释放。

      使用 erase 或 delete 或 free 操作应注意不要对同一地址/对象多次使用。

  • 解引用空指针/野指针

    对于空指针:先应该判断空指针,可以用 p == nullptr 或 !p

    对于野指针:可以释放指针的时候将其置为 nullptr 以规避。

  • 有符号数溢出

    例如我们有一个表达式 x+1 > x

    正常输出应当是 true,但是在 INT_MAX 作为 x 时输出 false,这时称为 signed integer overflow

    可以使用更大的数据类型(例如 long long 或 __int128),或判断溢出。若保证无负数,亦可使用无符号整型。

会导致 RE

  • 没删文件操作(某些 OJ)。

  • 排序时比较函数的错误 std::sort 要求比较函数是严格弱序:a<a 为 false;若 a<b 为 true,则 b<a 为 false;若 a<b 为 true 且 b<c 为 true,则 a<c 为 true。其中要特别注意第二点。 如果不满足上述要求,排序时很可能会 RE。 例如,编写莫队的奇偶性排序时,这样写是错误的:

    1
    2
    3
    4
    5
    6
    bool operator<(const int a, const int b) {
      if (block[a.l] == block[b.l])
        return (block[a.l] & 1) ^ (a.r < b.r);
      else
        return block[a.l] < block[b.l];
    }
    上述代码中 (block[a.l]&1)^(a.r<b.r) 不满足上述要求的第二点。 改成这样就正确了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    bool operator<(const int a, const int b) {
      if (block[a.l] == block[b.l])
        // 错误:不满足严格弱序的要求
        // return (block[a.l] & 1) ^ (a.r < b.r);
        // 正确
        return (block[a.l] & 1) ? (a.r < b.r) : (a.r > b.r);
      else
        return block[a.l] < block[b.l];
    }

会导致 TLE

  • 分治未判边界导致死递归。

  • 死循环。

    • 循环变量重名。

    • 循环方向反了。

  • BFS 时不标记某个状态是否已访问过。

  • 使用宏展开编写 min/max

    这种错误会大大增加程序的运行时间,甚至直接影响代码的时间复杂度。在初学者写线段树时尤为多见。

    常见的错误写法是这样的:

    1
    2
    #define Min(x, y) ((x) < (y) ? (x) : (y))
    #define Max(x, y) ((x) > (y) ? (x) : (y))

    这样写虽然在正确性上没有问题,但是如果直接对函数的返回值取 max,如 a = Max(func1(), func2()),而这个函数的运行时间较长,则会大大影响程序的性能,因为宏展开后是 a = func1() > func2() ? func1() : func2() 的形式,调用了三次函数,比正常的 max 函数多调用了一次。注意,如果 func1() 每次返回的答案不一样,还会导致这种 max 的写法出现错误。例如 func1() 为 return ++a; 而 a 为全局变量的情况。

    示例:如下代码会被卡到单次查询  导致 TLE。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    #define max(x, y) ((x) > (y) ? (x) : (y))
    
    int query(int t, int l, int r, int ql, int qr) {
      if (ql <= l && qr >= r) {
        ++ti[t];  // 记录结点访问次数方便调试
        return vi[t];
      }
    
      int mid = (l + r) >> 1;
      if (mid >= qr) return query(lt(t), l, mid, ql, qr);
      if (mid < ql) return query(rt(t), mid + 1, r, ql, qr);
      return max(query(lt(t), l, mid, ql, qr), query(rt(t), mid + 1, r, ql, qr));
  • 没删文件操作(某些 OJ)。

  • 在 for/while 循环中重复执行复杂度非  的函数。严格来说,这可能会引起时间复杂度的改变。

会导致 MLE

  • 数组过大。

  • STL 容器中插入了过多的元素。

    • 经常是在一个会向 STL 插入元素的循环中死循环了。

    • 也有可能被卡了。

会导致常数过大

  • 定义模数的时候,未定义为常量。

    • 示例:

      1
      2
      // int mod = 998244353;      // 错误
      const int mod = 998244353;  // 正确,方便编译器按常量处理
  • 使用了不必要的递归(尾递归不在此列)。

  • 将递归转化成迭代的时候,引入了大量额外运算。

只在程序在本地运行的时候造成影响的错误

  • 文件操作有可能会发生的错误:

    • 对拍时未关闭文件指针 fclose(fp) 就又令 fp = fopen()。这会使得进程出现大量的文件野指针。

    • freopen() 中的文件名未加 .in/.out

  • 使用堆空间后忘记 delete 或 free

 

常见技巧

本页面主要列举一些竞赛中的小技巧。

利用局部性

局部性是指程序倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。局部性分为时间局部性和空间局部性。

  • 循环展开。通过适当的循环展开可以减少整个计算中关键路径上的操作数量

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // for (int i = 0; i < n; ++i) {
    //     res = res OP a[i];
    //}
    // 不如
    int i;
    for (i = 0; i < n; i += 2) {
      res = res OP a[i];
      res = res OP a[i + 1];
    }
    for (; i < n; ++i) {
      res = res OP a[i];
    }
  • 重新结合变换,增加了可以并行执行的运算数量。

    1
    2
    3
    4
    // 加号可以换成其他的运算符
    for (int i = 0; i < n; ++i) res = (res + a[i]) + a[i + 1];
    // 不如
    for (int i = 0; i < n; ++i) res = res + (a[i] + a[i + 1]);

循环宏定义

如下代码可使用宏定义简化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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()) { ... }

另外推荐一个比较有用的宏定义:

1
#define _rep(i, a, b) for (int i = (a); i <= (b); ++i)

善用 namespace

使用 namespace 能使程序可读性更好,便于调试。

例题:NOI 2018 屠龙勇士
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// NOI 2018 屠龙勇士 40分部分分代码
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;
long long n, m, a[100005], p[100005], aw[100005], atk[100005];

namespace one_game {
// 其实namespace里也可以声明变量
void solve() {
  for (int y = 0;; y++)
    if ((a[1] + p[1] * y) % atk[1] == 0) {
      cout << (a[1] + p[1] * y) / atk[1] << endl;
      return;
    }
}
}  // namespace one_game

namespace p_1 {
void solve() {
  if (atk[1] == 1) {  // solve 1-2
    sort(a + 1, a + n + 1);
    cout << a[n] << endl;
    return;
  } else if (m == 1) {  // solve 3-4
    long long k = atk[1], kt = ceil(a[1] * 1.0 / k);
    for (int i = 2; i <= n; i++)
      k = aw[i - 1], kt = max(kt, (long long)ceil(a[i] * 1.0 / k));
    cout << k << endl;
  }
}
}  // namespace p_1

int main() {
  int T;
  cin >> T;
  while (T--) {
    memset(a, 0, sizeof(a));
    memset(p, 0, sizeof(p));
    memset(aw, 0, sizeof(aw));
    memset(atk, 0, sizeof(atk));
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> p[i];
    for (int i = 1; i <= n; i++) cin >> aw[i];
    for (int i = 1; i <= m; i++) cin >> atk[i];
    if (n == 1 && m == 1)
      one_game::solve();  // solve 8-13
    else if (p[1] == 1)
      p_1::solve();  // solve 1-4 or 14-15
    else
      cout << -1 << endl;
  }
  return 0;
}

使用宏进行调试

编程者在本地测试的时候,往往要加入一些调试语句。而在需要提交到 OJ 时,为了不使调试语句的输出影响到系统对程序输出结果的判断,就要把它们全部删除,耗时较多。这种情况下,可以通过定义宏的方式来节省时间。大致的程序框架是这样的:

1
2
3
4
5
6
7
8
#define DEBUG
#ifdef DEBUG
// do something when DEBUG is defined
#endif
// or
#ifndef DEBUG
// do something when DEBUG isn't defined
#endif

#ifdef 会检查程序中是否有 #define 定义的对应标识符,如果有定义,就会执行后面的语句。而 #ifndef 会在没有定义相应标识符的情况下执行后面的语句。

这样,只需在 #ifdef DEBUG 里写好调试用代码,#ifndef DEBUG 里写好真正提交的代码,就能方便地进行本地测试。提交程序的时候,只需要将 #define DEBUG 一行注释掉即可。也可以不在程序中定义标识符,而是通过 -DDEBUG 的编译选项在编译的时候定义 DEBUG 标识符。这样就可以在提交的时候不用修改程序了。

不少 OJ 都开启了 -DONLINE_JUDGE 这一编译选项,善用这一特性可以节约不少时间。

对拍

对拍是一种进行检验或调试的方法,通过对比两个程序的输出来检验程序的正确性。可以将自己程序的输出与其他程序的输出进行对比,从而判断自己的程序是否正确。

对拍过程要多次进行,因此需要通过批处理的方法来实现对拍的自动化。

具体而言,对拍需要一个 数据生成器 和两个要进行输出结果比对的程序。

每运行一次数据生成器都将生成的数据写入输入文件,通过重定向的方法使两个程序读入数据,并将输出写入指定文件,最后利用 Windows 下的 fc 命令比对文件(Linux 下为 diff 命令)来检验程序的正确性。如果发现程序出错,可以直接利用刚刚生成的数据进行调试。

对拍程序的大致框架如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int main() {
  // For Windows
  // 对拍时不开文件输入输出
  // 当然,这段程序也可以改写成批处理的形式
  while (true) {
    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文件中,可以直接利用进行调试
    }
  }
}

内存池

当动态分配内存时,频繁使用 new/malloc 会占用大量的时间和空间,甚至生成大量的内存碎片从而降低程序的性能,可能会使原本正确的程序 TLE/MLE。

这时候需要使用到“内存池”这种技巧:在真正使用内存之前,先申请分配一定大小的内存作为备用。当需要动态分配时直接从备用内存中分配一块即可。

在大多数 OI 题当中,可以预先算出需要使用到的最大内存并一次性申请分配。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 申请动态分配 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;
}
posted @   Dawning_moon  阅读(348)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示