跳转到正文
zeno's blog
返回

C++ 竞赛:ACM 模式 I/O 的组合拳

Table of contents

Open Table of contents

TL;DR

ACM 模式写 C++ 的 I/O 模板只有两行:ios_base::sync_with_stdio(false); cin.tie(nullptr);——关掉 C stdio 同步和 cin 对 cout 的 tie,把默认慢的 cin/cout 提到接近 scanf/printf 的速度;然后用 cin >> 读 token、getline 读整行、stringstream 切分变长字段 这三招覆盖 95% 的输入格式;剩下 5% 的极限数据量(> 10^7 整数)用 fread + 手写 parseInt 解决。最容易踩的坑是 cin >>getline 混用时换行符残留、scanf 读 double 用错 %f、输出循环里写 endl 拖慢几十倍。


为什么 ACM 对 I/O 特别敏感

工程开发里 I/O 性能通常不是瓶颈,但 ACM 不同:

  1. 数据量大 — 10^6 ~ 10^7 个整数是常态。默认的 cin/cout 读 10^6 个 int 可能要 500ms+,直接把 1s 时限的题 TLE 掉
  2. 输入格式多样 — 定长矩阵、变长列表、带空格的字符串、多组测试数据、读到 EOF 停止,每种都有对应的最佳方案
  3. 时限严格 — I/O 吃掉 50% 时间的情况很常见,算法再好也跑不完

核心矛盾:I/O 慢到可以让同一份算法在 AC 和 TLE 之间来回切换。所以必须把 I/O 当成和算法同等重要的工程问题来处理。


第一层:默认的 cin/cout 为什么慢

两个独立原因:

1. 与 C stdio 同步(sync_with_stdio)

C++ 标准要求 std::cin/cout 默认与 C 的 scanf/printf 共享同一套缓冲区,这样两种 I/O 混用时顺序正确。代价:cin/cout 每次操作都要额外协调 C stdio 的缓冲区,实测比纯 scanf/printf 慢 3~10 倍。

2. cin 与 cout 绑定(tie)

默认 std::cintiestd::cout,含义是:每次从 cin 读输入前,先 flush cout 的缓冲区。这是为了让交互程序正确显示提示:

cout << "Enter name: ";   // 未 flush,还在缓冲区
cin >> name;              // 读之前自动 flush cout,用户才看得到提示

对 ACM 批处理场景这个 flush 完全没用,纯开销。


加速三连:写在 main 第一行

#include <iostream>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(nullptr);
    // 正文

    return 0;
}

再加上一条:非交互题永远用 "\n" 代替 std::endlendl 除了输出换行还会 flush 整个输出缓冲区,在输出 10^6 行时能把耗时从几十毫秒拖到几秒。

做完这三步,cin/cout 速度和 scanf/printf 差距在 20% 以内,绝大多数题够用。

代价(必须知道):


输入模式大全:覆盖 95% 情况的三把刀

模式 1:读定长 token(整数 / 浮点数 / 单词)

int n;
cin >> n;

double x, y;
cin >> x >> y;

// 等价 scanf
scanf("%d", &n);
scanf("%lf %lf", &x, &y);   // double 必须用 %lf

cin >> 会跳过前导空白(空格、tab、换行),读到下一个空白停下。

模式 2:读定长数组 / 矩阵

int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];

int r, c;
cin >> r >> c;
vector<vector<int>> g(r, vector<int>(c));
for (int i = 0; i < r; ++i)
    for (int j = 0; j < c; ++j)
        cin >> g[i][j];

cin >> 对空白不敏感,不管输入是一行放完还是分多行放,都读得对。

模式 3:读含空格的整行字符串

string s;
getline(cin, s);   // 读到换行为止,不包含换行符

不能用 cin >> s —— 它遇到空格就停,读不完整行。

模式 4:读变长一行(不知道这一行有几个数)

ACM 最常被卡的场景,比如输入”每行一个列表,长度不定”:

3 1 4 1 5 9
2 6 5
8 9 7 9

标准解法:getline 取整行,再用 stringstream 按空白切分:

#include <sstream>

string line;
getline(cin, line);
stringstream ss(line);
int x;
vector<int> row;
while (ss >> x) row.push_back(x);

stringstream>>cin >> 语义一致,按空白分隔读 token,读不到时 ss >> x 为假、循环退出。这是 getline + stringstream 的唯一通用组合,没有它就没有变长行输入。

模式 5:读到 EOF 停止(多组测试数据,不给组数)

经典模板题”A + B”就这样:

int a, b;
while (cin >> a >> b) {
    cout << a + b << "\n";
}

// 等价 scanf:检查返回值(成功匹配的字段数)
while (scanf("%d %d", &a, &b) == 2) { ... }

cin >> x 返回 cin 的引用,在 while 条件中隐式转为 bool:EOF 或类型不匹配时为 false。

模式 6:读固定组数 T 的测试数据

int T;
cin >> T;
while (T--) {
    int n;
    cin >> n;
    // 每组独立处理
}

模式 7:按行读到 EOF

string line;
while (getline(cin, line)) {
    // 处理一行
}

getline 同样返回流引用,可用于 while 判 EOF。


致命坑:cin >> 和 getline 混用

这是 ACM 和课程题最常被坑的问题,没有之一:

int n;
cin >> n;        // 读完 n,换行符 \n 还留在缓冲区!

string line;
getline(cin, line);   // 立刻读到一个空行(就是那个残留的 \n)

根因cin >> n 按格式读,遇到空白(包括 \n)停下但不消耗它。下一个 getline\n 开始读,立刻遇到换行返回空串。

正确做法cin >> n 之后显式吃掉到行尾的所有字符:

#include <limits>

int n;
cin >> n;
cin.ignore(numeric_limits<streamsize>::max(), '\n');   // 吃到下一个 \n(含)为止

string line;
getline(cin, line);

简化但不严格的写法:cin.ignore(); 只吃掉一个字符——只要确认下一个字符一定是 \n 就够用。严格写法能处理 n 后面可能还有空格的情况。

反向场景同样要小心getline 后接 cin >> 没问题,因为 getline 已经把 \n 消费掉了。


scanf / printf 速查表

需要极致速度或输入格式复杂(带特定分隔符)时,直接用 C I/O。

类型scanfprintf
int%d%d
long long%lld%lld
unsigned int%u%u
unsigned long long%llu%llu
float%f%f
double%lf%f%lf
long double%Lf%Lf
char%c%c
char*%s必须限长 %99s%s
16 进制 int%x%x

置信度说明:%lld 在 GCC/Clang/MSVC 现代版本都支持;老 MSVC 的 %I64d 已经过时(需验证——大约在 VS2013+ 后 %lld 就通用了)。

格式化输出示例:

printf("%.6f\n", x);       // 保留 6 位小数
printf("%d %d\n", a, b);
printf("%5d\n", 42);       // 右对齐宽度 5:"   42"
printf("%-5d|\n", 42);     // 左对齐宽度 5:"42   |"
printf("%05d\n", 42);      // 前补零宽度 5:"00042"

特定分隔符输入(scanf 独有的强项):

// 读 "2026-04-11"
int y, m, d;
scanf("%d-%d-%d", &y, &m, &d);

// 读 "12:30:45"
int h, mi, s;
scanf("%d:%d:%d", &h, &mi, &s);

scanf 格式字符串里% 字符必须字面匹配输入。cin >> 做不到这一点,遇到这种格式只能 getline 后手动解析。

关键限制scanf("%s", ...) 读 C 字符数组,不能直接读 std::string

string s;
scanf("%s", &s);        // ❌ 未定义行为
char buf[1000];
scanf("%999s", buf);    // ✅ 限长防溢出
s = buf;

cout 格式化输出:iomanip

#include <iomanip>

cout << fixed << setprecision(6) << 3.14159265 << "\n";  // 3.141593
cout << setw(5) << 42 << "\n";                 // "   42"
cout << setw(5) << left << 42 << "|\n";        // "42   |"
cout << setfill('0') << setw(5) << 42 << "\n"; // "00042"
cout << hex << 255 << "\n";    // ff
cout << oct << 8 << "\n";      // 10
cout << dec;                   // 切回 10 进制

关键区别fixed / setprecision / hex 等是粘性状态,一次设置影响后续所有输出,不像 printf 每次独立。setwsetfill一次性的,每次输出前都要重新设置。


极限性能:fread 手写快读

当数据量达到 10^7 个整数、连 scanf 都 TLE 时,下探到 fread 层:

#include <cstdio>
#include <cctype>

namespace fastio {
    constexpr int BUF_SIZE = 1 << 20;   // 1 MB
    char buf[BUF_SIZE];
    int buf_pos = 0, buf_len = 0;

    inline char gc() {
        if (buf_pos == buf_len) {
            buf_len = (int)fread(buf, 1, BUF_SIZE, stdin);
            buf_pos = 0;
        }
        return buf_pos == buf_len ? EOF : buf[buf_pos++];
    }

    inline int readInt() {
        int x = 0, sign = 1;
        char c = gc();
        while (!isdigit(c) && c != '-' && c != EOF) c = gc();
        if (c == '-') { sign = -1; c = gc(); }
        while (isdigit(c)) { x = x * 10 + (c - '0'); c = gc(); }
        return x * sign;
    }
}

int main() {
    int n = fastio::readInt();
    // ...
}

为什么快:绕过 stdio 的 locale / 格式字符串解析开销,一次 fread 读一整块到用户缓冲区,再用最简单的状态机扫过。经验值比 scanf 快 2~5 倍(需验证——具体倍数因编译器和数据形态而异)。

什么时候用

不要无脑套。90% 的题 sync_with_stdio(false) + cin 够了,手写快读的维护成本不小。


交互题的特殊处理

交互题(你输出一次,评测机读后回给你下一个查询)的核心要求是每次输出后必须 flush,否则输出留在缓冲区里,评测机收不到就死锁——本地看起来一切正常,提交上去直接 TLE。

// 交互题配置
ios_base::sync_with_stdio(false);
// cin.tie(nullptr);   // ❌ 交互题禁用,或者就让它默认 tie 到 cout

// 每次输出后显式 flush
cout << query << endl;           // endl 自带 flush,交互题反而要用
// 或
cout << query << "\n" << flush;
// 或
cout << query << "\n";
cout.flush();

口诀:非交互题用 "\n" 避免 flush;交互题用 endl 强制 flush。方向相反。


实战模板

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<int> a(n);
        for (auto& x : a) cin >> x;
        // 解题逻辑 ...
        cout << "result" << "\n";
    }
    return 0;
}

<bits/stdc++.h> 是 GCC 扩展(一次性 include 所有标准库),非标准但 Codeforces / LeetCode / AtCoder 等平台都支持。生产代码别用——编译慢,且 MSVC 不支持。

本地调试小技巧:用 freopen 重定向 stdin/stdout 到文件,跑真实数据:

#ifdef LOCAL
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif

编译时加 -DLOCAL 启用,提交时自动关闭。


Pitfalls 汇总

1. 关闭 sync 后混用 cin/cout 和 scanf/printf

ios_base::sync_with_stdio(false);
cin >> n;
printf("%d\n", n);   // 输出顺序可能乱

为什么是坑:关闭 sync 后,C 和 C++ 的缓冲区独立,flush 时机不同,交错写的内容在终端/文件里顺序可能不是你期望的。 怎么避免:要么只用 cin/cout,要么不关 sync。不要跨阵营。

2. 循环里用 endl 代替 “\n”

for (int i = 0; i < 1000000; ++i) cout << i << endl;   // ❌ 每行 flush
for (int i = 0; i < 1000000; ++i) cout << i << "\n";   // ✅

为什么是坑:10^6 行输出下,endl 能把输出时间从几十 ms 拖到几秒,轻松 TLE。 怎么避免:非交互题永远用 "\n"endl 只在必须 flush 的场景(交互题、调试输出、程序结束前)才用。

3. scanf 读 double 用了 %f

double x;
scanf("%f", &x);   // ❌ 读出来是垃圾值
scanf("%lf", &x);  // ✅

为什么是坑scanf%f 告诉它按 float(4 字节)写入,而 double 是 8 字节——内存布局错位,读出来完全不是那个数。特别恶心的是 C++ 编译器通常不警告,只有加 -Wformat 才提示。和 printf 的规则(%f 接受 double)不对称。 怎么避免:记硬规则——scanf 读 double 用 %lf,printf 输出 double 用 %f%lf 都行。

4. cin >> 之后 getline 读到空行

见上面专节”致命坑”。根因是 cin >> 不消耗 \n怎么避免:混用前一律 cin.ignore(numeric_limits<streamsize>::max(), '\n');

5. scanf(“%s”, …) 尝试读 std::string

string s;
scanf("%s", &s);   // ❌ UB

为什么是坑%s 只认 char*std::string 对象不是 C 字符串,写进去会破坏 string 的内部结构。 怎么避免:用 cin >> s,或先读到 char[] 再赋值。

6. 读字符串没设最大长度

char buf[100];
scanf("%s", buf);      // ❌ 缓冲区溢出风险
scanf("%99s", buf);    // ✅ 最多读 99 字符 + '\0'

为什么是坑:和 C 里的 gets 同类问题——输入比缓冲区长时写越界,非法内存写入。ACM 题虽然输入可控,但养成限长习惯避免意外。 怎么避免:永远写 %99s 形式的限长修饰。

7. 变长行用 cin >> 而不是 getline + stringstream

// 输入:第一行是个数 n,第二行是 n 个数
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x;   // ✅ 这种能用

但如果输入是”每行一个不定长列表”,cin >> x 不知道哪里是行尾,会贪心读到下一行。 怎么避免:凡是”一行代表一个逻辑单元、一行内字段数不定”的输入,必须 getline + stringstream

8. 关了 sync 还在调 fflush(stdout)

ios_base::sync_with_stdio(false);
cout << "hello";
fflush(stdout);   // ❌ 对 cout 的缓冲区无效

为什么是坑fflush 操作的是 C stdio 的缓冲区,关闭 sync 后 cout 有自己独立的缓冲区,fflush 碰不到它。 怎么避免:用 cout.flush()cout << flush / cout << endl

9. cin >> 读字符数组时没判大小

char s[100];
cin >> s;   // ❌ 超长会溢出
cin >> setw(100) >> s;   // ✅ 限长,和 scanf("%99s") 等价

为什么是坑:同 pitfall 6,C 风格缓冲区溢出。cin >> 默认无限长读。 怎么避免:用 std::string 代替 char[]string 会自动扩容。

10. 忘记 cout 浮点精度默认只有 6 位

double x = 1.0 / 3;
cout << x << "\n";                                // 0.333333(仅 6 位)
cout << fixed << setprecision(15) << x << "\n";   // 0.333333333333333

为什么是坑:竞赛题经常要 10^-9 精度,忘记设 setprecision 直接 WA。默认 6 位是 C++ 标准规定的,不是实现 bug。 怎么避免:主函数开头就设好 cout << fixed << setprecision(k),k 至少 10。


选型 Checklist

场景首选备注
一般算法题(n ≤ 10^6)cin/cout + 关闭 sync最省心
数据量 10^6 ~ 10^7 整数scanf/printfcin 也能试,scanf 更稳
数据量 > 10^7fread 快读维护成本更高,按需使用
交互题cout << endl不关 tieflush 是生命线
变长行输入getline + stringstream唯一通用方案
带特定分隔符(日期、时间、CSV)scanf 格式字符串cin 处理不了
输出浮点固定精度cout << fixed << setprecision(k)printf("%.kf")别忘设置
读含空格的字符串getline(cin, s)cin >> 遇空格就停
混用 C 和 C++ I/O不关 sync否则输出顺序乱

延伸方向


信息来源和置信度


分享这篇文章:

上一篇
Go 网络(一):goroutine-per-connection 模型与生产实践
下一篇
C++ STL:迭代器如何解耦算法与容器