PART 0算法竞赛指南· 实战工程技巧
Accepted · 实战工程技巧/ 0.1 → 0.9
第〇部分 · PART 0

实战工程技巧

正课算法之前的「实战工程技巧」——输入输出、数据溢出、看代码揪错、评测结果解读、编译报错、对拍调试、复杂度估算、偷分与比赛策略。

这些不是算法本身,却是把「会做」变成「拿到分」的关键:少挂罚时、少踩坑、把会做的题又快又稳地切掉。铜牌的胜负,很多就藏在这里。

第〇部分 · 导读

本课导读 · 知识体系大纲与两个月计划

先给你吃颗定心丸:两个月从你现在的基础(C/C++ + 贪心/二分/前缀和/DP/高精度等模板都摸过)冲区域赛铜牌水平,是现实可达的。铜牌的本质不是会多少冷门算法,而是把那 3~5 道偏简单的题又快又稳地切掉、不挂罚时。所以我们的策略很明确:少而精的知识点 + 大量做题 + 反复打模拟赛。

下面是我提议的完整大纲框架两个月计划。你先看,咱们把它定下来,之后我再一个分支一个分支地展开细讲。


一、知识体系大纲(按铜牌优先级标注)

标注说明:【核心·必修】 铜牌必须吃透 | 【强化】 稳定输出靠它 | 【拓展】 银牌往上,两月内先放着

第〇部分:竞赛实战工程技巧 ← 你这次主要问的就是这块

这部分不教算法,但决定你「会做的题能不能拿到分」,价值极高,会单独花一整块讲。

  • 0.1 输入输出技巧(scanf/printf vs cin/cout、关同步加速、字符串/整行读入、读到 EOF、多组数据、输出格式与精度)
  • 0.2 数据类型与溢出(int/long long/unsigned 怎么选、什么时候必须开 long long、取模运算)
  • 0.3 常见易错点清单(数组越界、忘初始化、边界条件、浮点比较、多测不清空)
  • 0.4 评测结果解读与对策(AC/WA/TLE/MLE/RE/CE/PE/OLE 逐个讲遇到怎么办)
  • 0.5 编译错误怎么读(常见报错中英对照,怎么快速定位到那一行)
  • 0.6 调试与快速查错(对拍 / 边界数据 / 二分定位 / 防御性写法)
  • 0.7 时间空间复杂度分析(从数据范围反推该用什么算法——这是最实用的技巧)
  • 0.8 偷分 / 拿部分分策略(暴力、打表、特判、贪心猜结论、骗分)
  • 0.9 比赛策略(读题顺序、开题取舍、卡题止损、罚时管理、队内配合)

第一部分:基础算法与 STL 【核心·必修】

1.1 STL 精通(vector/string/pair/sort/set/map/priority_queue/lower_bound…)· 1.2 排序与二分(含二分答案)· 1.3 前缀和与差分· 1.4 双指针/尺取· 1.5 贪心(你有基础,补经典模型)· 1.6 递归与分治· 1.7 栈/队列/单调栈/单调队列/堆

第二部分:搜索 【核心·必修】

2.1 DFS/BFS · 2.2 回溯(全排列/子集)· 2.3 剪枝· 2.4 记忆化搜索(通往 DP)· 2.5 双向 BFS / IDA\* 【拓展】

第三部分:动态规划 【核心·必修,重中之重】

3.1 DP 思维(状态/转移/边界)· 3.2 线性 DP(LIS/LCS/最大子段和)· 3.3 背包(01/完全/多重)· 3.4 区间 DP · 3.5 树形 DP · 3.6 状压 DP 【强化】· 3.7 数位 DP / DP 优化 【拓展】

第四部分:图论 【核心·必修】

4.1 图的存储(邻接表/链式前向星)· 4.2 并查集· 4.3 最短路(堆优化 Dijkstra / SPFA / Floyd)· 4.4 最小生成树(Kruskal/Prim)· 4.5 拓扑排序· 4.6 连通分量/二分图/网络流 【拓展】

第五部分:数学 【核心·必修】

5.1 数论基础(GCD/LCM/快速幂/快速乘)· 5.2 素数(筛法/分解质因数)· 5.3 同余与逆元(费马小定理/扩展欧几里得)· 5.4 组合数学(排列组合/组合数取模)· 5.5 高精度(你有基础)· 5.6 概率期望/矩阵快速幂/线性基 【拓展】

第六部分:字符串 【强化】

6.1 字符串哈希· 6.2 KMP · 6.3 Trie · 6.4 Manacher/AC 自动机/后缀结构 【拓展】

第七部分:进阶数据结构 【强化】

7.1 树状数组· 7.2 线段树(入门)· 7.3 离散化/差分约束/平衡树 【拓展】

铜牌冲刺重点圈定:第〇 + 第一 + 第二 + 第三(线性/背包) + 第四(并查集/最短路/MST/拓扑) + 第五(数论/组合基础) + 第六(哈希/KMP) + 第七(树状数组入门)。其余【拓展】项两个月内先不碰,集中火力。


二、两个月冲刺计划(8 周)

每天节奏建议:1 小时学概念 + 写模板 → 2~3 小时做题(先专题后混合)。周末固定打一场虚拟赛(VP),赛后必须补题。你基础不少东西见过,所以前期重点不是「第一次学」,而是提速 + 提准 + 学会在比赛里用

  • 第 1 周 第〇部分全套 + STL + 排序二分。打通工具链(选好评测平台、配好对拍)。这周把「输入输出/查错/读编译报错/复杂度反推」练成肌肉记忆。
  • 第 2 周 前缀和差分 + 双指针 + 贪心强化 + 数学入门(快速幂/GCD/素数筛)。
  • 第 3 周 搜索:DFS/BFS + 回溯 + 剪枝。
  • 第 4 周 基础 DP:线性 + 背包(这周要做到看到就有感觉)。
  • 第 5 周 图论:并查集 + 最短路 + MST + 拓扑。
  • 第 6 周 区间/树形 DP 入门 + 组合数取模/逆元。
  • 第 7 周 字符串(哈希/KMP)+ 树状数组/线段树入门。
  • 第 8 周 纯实战:刷区域赛真题套题、连打模拟赛、查漏补缺、把高频题型做到条件反射。

推荐平台:Codeforces(Div3/Div4 练手、Gym 打真题)、AtCoder ABC(A~E 难度梯度极适合入门)、洛谷(中文题解 + 现成题单)、牛客(CCPC/ICPC 中文真题)、vjudge(组套题打 VP);查资料认准 OI-Wiki(oi-wiki.org,免费且权威)。

关于目标的实话:这个计划能把你的个人水平练到「铜牌该会的题能稳定切」。最终能不能拿牌还受队友、赛题难度、临场发挥影响,但技能层面,铜牌是合理且大概率够得着的目标。


这就是我提议的整体框架。在我们开始逐个展开之前,请你确认或调整:

  1. 这个大纲和优先级你认可吗? 有没有想加 / 想砍的部分?
  2. 你想从哪里开始? 我的强烈建议是先啃第〇部分(实战工程技巧)——也就是你这次问的输入输出、查错、复杂度、偷分那些,因为它立刻能让你「少挂分」,性价比最高。

你点个头或说说想从哪开始,我就把那个分支详细铺开给你。

第〇部分 · 0.1

0.1 输入输出技巧

好,第〇部分最实用的一块,咱们一次讲透。核心思路是:给你一套固定的默认写法,让你 95% 的题目根本不用思考 I/O,把脑子留给算法。

1. 先背下这个万能开头

每道题都这样开头,闭着眼睛敲:

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

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

    // 你的代码

    return 0;
}

三行的作用:

  • #include <bits/stdc++.h> 一次性包含所有标准库,不用再纠结 <vector><algorithm><iomanip> 之类要不要写(竞赛专用,工程里别这么干)。
  • ios::sync_with_stdio(false); 切断 C++ 流和 C 的 scanf/printf 的同步,cin/cout 速度直接拉满,能从「慢到 TLE」变成「够快」。
  • cin.tie(nullptr); 解除 cin 和 cout 的绑定。默认每次 cin 前都会先刷新 cout,解绑后省掉这个开销。

记住配套的第一条铁律

永远用 "\n" 换行,不要用 endl

endl 每次都会强制刷新缓冲区,输出几十万行时能把你从 100ms 拖到 TLE。"\n" 只是个换行符,快得多。

2. cin/cout 还是 scanf/printf?

我的建议很明确:默认全程用 cin/cout,统一一种心智模型,少踩格式符的坑。 加了上面那两行后,cin/cout 的速度对铜牌范围的题完全够用。

但你要知道 scanf/printf 存在,因为队友会用、网上模板会用。它俩唯一的麻烦是格式符容易记错,这是新手 WA 的重灾区:

类型scanf 读入printf 输出
int%d%d
long long%lld%lld
double%lf%f ← 注意不一样!
char%c%c
字符数组%s%s

最坑的两个:

  • double 在 scanf 里是 %lf,在 printf 里是 %f。读用 %lf、印用 %f,写反了就是一堆乱码或 0。
  • long long 必须用 %lld,用 %d 读会读错、印会出垃圾值。

还有第二条铁律

开了 sync_with_stdio(false) 之后,绝对不要再混用 scanf/printf/getchar/puts 要么全 cin/cout,要么把那两行删掉全用 scanf/printf。混用会导致输入顺序错乱,调到怀疑人生。

3. 数字读入的三种标准姿势

竞赛输入格式翻来覆去就这三种,背熟即可:

(a) 读到文件结尾(不告诉你有几组)——题面常写 "处理到 EOF":

solution.cpp
int a, b;
while (cin >> a >> b) {   // 读不到了自动退出
    cout << a + b << "\n";
}

cin >> 读失败(到 EOF)会返回 false,循环自然结束。对应的 scanf 写法是 while (scanf("%d %d", &a, &b) != EOF)

(b) 第一行给组数 T——最常见:

solution.cpp
int T;
cin >> T;
while (T--) {
    // 每组的处理逻辑
}

(c) 先读 n,再读 n 个数

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

注意 cin >> 会自动跳过空格和换行,所以数字之间是空格还是换行隔开,你都不用管,照读就行。

4. 字符串读入(踩坑最多,重点看)

先分清两个工具:

  • cin >> s:读一个词,遇到空格/换行就停,并且会自动跳过前面的空白
  • getline(cin, s):读一整行(含空格),遇到换行停,并把那个换行符吃掉。

那个让无数新手抓狂的 bug

当你cin >> ngetline 时:

solution.cpp
int n;
cin >> n;            // 读了数字,但行末的换行符 '\n' 还留在缓冲区里
string line;
getline(cin, line);  // 它读到的是那个残留的换行 → line 是空的!

cin >> n 只取走数字,把后面的 \n 留在了缓冲区,结果第一次 getline 直接撞上这个 \n,读到一个空行。

最好记的解决办法,记这一个就够:

solution.cpp
int n;
cin >> n;
string line;
getline(cin >> ws, line);  // >> ws 先吃掉所有残留空白,再读整行

cin >> ws 会跳过一切前导空白(空格、换行、Tab),干净利落。把它当成「getline 前的护身符」。

常用字符串场景

读一个词、读整行、读到 EOF 的标准写法:

solution.cpp
string s;
cin >> s;                          // 读一个单词

string line;
getline(cin, line);                // 读一整行

while (getline(cin, line)) {       // 一行一行读到文件尾
    // 处理 line
}

小结一下你的默认套路:能用 cin >> s 就用它;只有题目明确「这一行可能含空格、要整行读」时才上 getline,而且 getline 前面加 >> ws 这样基本不会出错。

5. 输出格式与精度

整数、字符串直接 cout << x << "\n" 就行,重点是浮点数

最简单的方式——cout 设定一次,后面一直生效:

solution.cpp
cout << fixed << setprecision(2) << x << "\n";  // 保留 2 位小数,如 3.14

fixed 表示用定点小数(不要科学计数法),setprecision(2) 是小数位数。设一次之后所有浮点输出都按这个格式来。对应 printf 写法是 printf("%.2f\n", x)

浮点的第三条铁律

浮点数永远不要用 == 比较。fabs(a - b) < 1e-9 判断相等。

另外两个精度小坑:

  • 题目说「误差不超过 1e-6」时,输出位数要够,setprecision(10) 往上给,别只给 2 位。
  • 当心输出 -0.00。某些情况下计算结果是极小的负数,会印成 -0.00 导致 WA,必要时特判一下或加个极小的 eps。

6. 易错点速查表

每次提交前心里过一遍:

  • 该不该开 long long 数据范围 ~1e5 但要算乘积或求和时,int 几乎必爆。拿不准就开 long long
  • 用了 scanf:long long 是不是写成了 %llddouble 读入是不是 %lf
  • 换行是不是用了 "\n" 而不是 endl
  • 开了 sync 之后是不是手贱混用了 scanf/printf。
  • getline 前面是不是漏了 >> ws
  • 浮点比较是不是用了 ==(该用 eps)。
  • 数字之间、行之间的分隔(一般单空格 + 换行,绝大多数判题机会自动忽略行末多余空格)。

这块就是这些,把第 1 节的模板和第 4 节的字符串规则练成肌肉记忆,I/O 基本就不会再坑你了。

教练衔接coach

要不要我接着讲 0.2 数据类型与溢出(什么时候必须开 long long、取模运算怎么不出错)?还是你想先就上面某个点做几道题练手、我给你挑题?

你的提问student

我记得string里面还有很多那种很零碎的东西,感觉很烦恼,像什么字符串的长度,这些函数要记吗?像同一个字符输多少个,等等

问得特别好,这正是新手对 string 最大的误区——以为要把几十个成员函数背下来。其实完全不用。竞赛里 string 真正高频的就十来个操作,剩下的用到了现查 OI-Wiki 就行。

我的原则:string 当成「能用下标访问、还能自动变长的字符数组」,绝大多数事情靠下标 s[i] 和循环就解决了,根本不需要记花哨函数。

一、必须记住的(就这些,背下来)

1. 长度:s.size()s.length()

俩完全一样,记一个就行。我建议统一用 s.size(),因为 vector、set、map 全都是 .size(),统一心智。

solution.cpp
string s = "hello";
cout << s.size();      // 5
for (int i = 0; i < s.size(); i++) cout << s[i];  // 逐字符遍历

⚠️ 一个大坑s.size() 的类型是无符号整数(size_t)。写 for (int i = s.size() - 1; i >= 0; i--) 倒序遍历时,如果 s 是空串,s.size()-1 会变成一个超大正数(不是 -1),直接越界。对策:要么提前判空,要么写成 for (int i = (int)s.size() - 1; ...) 强转一下。

2. 下标访问:s[i]

最重要的操作,没有之一。s[i] 就是第 i 个字符(从 0 开始),可读可写:

solution.cpp
s[0] = 'H';            // 直接改
char c = s[2];         // 直接取

3. 末尾加字符 / 拼接:+=

solution.cpp
string s = "ab";
s += 'c';              // "abc"  ——加单个字符
s += "de";             // "abcde"——加一段字符串

往末尾加字符还有个 s.push_back('c'),和 += 单字符等价,记一个即可。

4. 判空:s.empty()

solution.cpp
if (s.empty()) ...     // 等价于 s.size() == 0,但更清晰

5. 比较:直接用 == < >

这是 string 的福利,不像数字浮点那样有坑,直接比就行:

solution.cpp
if (s == "yes") ...    // 内容相等
if (s1 < s2) ...       // 字典序比较,sort 字符串数组靠的就是它

6. 排序:sort(s.begin(), s.end())

把字符串内部的字符从小到大排,竞赛里出奇地常用(判断是否同字母异序词等):

solution.cpp
sort(s.begin(), s.end());   // "dcba" → "abcd"

到这里,你已经覆盖了 80% 的字符串题。 下面是「用到再查」的部分。

二、用到了再查的(眼熟即可,别背)

这些知道「有这么个东西」就行,需要时回来翻或查 OI-Wiki:

  • s.substr(pos, len)——从 pos 开始截取 len 个字符(不写 len 就截到末尾)。
  • s.find("ab")——找子串第一次出现的位置,找不到返回 string::npos
  • reverse(s.begin(), s.end())——翻转整个字符串。
  • to_string(123)——数字转字符串;stoi(s) / stoll(s)——字符串转 int / long long。
  • count(s.begin(), s.end(), 'a')——数某个字符出现了几次。

记不住完全没关系,比赛时 substr find 的参数顺序我自己都常要回想一下,查就完事了。

三、你问的「同一个字符输出多少个」——这个有专门写法

比如要输出 5 个星号 *****根本不用循环string 有个超好用的构造:

solution.cpp
string stars(5, '*');      // 直接得到 "*****"
cout << stars;

// 或者一行
cout << string(5, '*');    // 输出 *****
教练衔接coach

string(个数, 字符) 就是「把某字符重复 n 遍」。画三角形、铺空格对齐这种题,这个写法又短又不出错,强烈建议记住。

solution.cpp
// 经典:输出一个高度为 n 的左对齐星号三角
for (int i = 1; i <= n; i++)
    cout << string(i, '*') << "\n";

四、配套要记的:字符本身的小技巧

字符串题经常要对单个字符做判断,这几个比背函数更值得记,因为是通用套路

solution.cpp
// 判断类型(需要 <cctype>,bits/stdc++.h 已含)
isdigit(c)    // 是不是数字 '0'~'9'
isalpha(c)    // 是不是字母
islower(c) / isupper(c)   // 是不是小写/大写

// 大小写转换
tolower(c) / toupper(c)   // 返回转换后的字符

// 字符 ↔ 数字(最常用!)
int d = c - '0';          // 字符 '7' → 数字 7
char c = d + '0';         // 数字 7 → 字符 '7'

// 字母转 0~25 编号(哈希、计数常用)
int idx = c - 'a';        // 'c' → 2

最后两组(c - '0'c - 'a')是字符串题的命脉,几乎每道题都要用,务必形成条件反射。


总结成一句话string 你只需要扎实掌握「s.size() + s[i] 下标 + += 拼接 + == 比较 + sort + string(n, ch) 重复」这一小撮,再加上 c - '0' / c - 'a' 这俩字符技巧,就能应付铜牌阶段几乎所有字符串题。其余的,忘了是正常的,查就对了——这不丢人,是正确的做题方式。

教练衔接coach

要不要我给你出 3~4 道只考这些基础字符串操作的小题练练手,把这些点立刻盘活?或者继续往下讲 0.2 数据类型与溢出?


配套练习题

好,给你出 4 道题,专门盯着 0.1 的几个核心坑:多组数据、读到 EOF、整行读入(>> ws)、浮点精度、string(n,ch) 重复。难度都不大,重点是把 I/O 套路练成肌肉记忆。

建议你先全部自己写,写完再看我后面折叠的提示和参考代码。


第 1 题:A + B 多组版(练 while(T--) 和 EOF 两种姿势)

题意:输入有多组数据,每组一行两个整数 a b,输出 a+b处理到文件结尾(EOF),不告诉你有几组。

sample.txt
输入样例
1 2
10 20
100 200

输出样例
3
30
300

做完追加要求:再写一个「第一行给组数 T」的版本(输入第一行是 3,后面跟 3 行)。两种你都要会。


AC第 1 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    int a, b;
    while (cin >> a >> b) {       // 读到 EOF 自动停
        cout << a + b << "\n";
    }
    return 0;
}

T 组版本:

solution.cpp
int T;
cin >> T;
while (T--) {
    int a, b;
    cin >> a >> b;
    cout << a + b << "\n";
}

第 2 题:平均分(练浮点精度 fixed + setprecision

题意:第一行一个整数 n,第二行 n 个整数,求平均值,保留 2 位小数输出。

sample.txt
输入样例
4
1 2 3 4

输出样例
2.50

坑点自查:求和会不会溢出?平均值是整数除法还是浮点除法?(故意挖的,想想 10/410.0/4


AC第 2 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    int n;
    cin >> n;
    long long sum = 0;              // 求和开 long long 更稳
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        sum += x;
    }
    double avg = (double)sum / n;   // 关键:转成 double 再除,否则是整数除法
    cout << fixed << setprecision(2) << avg << "\n";
    return 0;
}

最常见的错:写成 sum / n 得到的是整数(比如 10/4 = 2),再赋给 double 也救不回来,必须在除法之前就转 double。

第 3 题:回显每一行(练 getline 读整行 + 含空格)

题意:第一行一个整数 n,接下来 n 行,每行是一句话(含空格),原样输出每一行,并在前面加上行号。

sample.txt
输入样例
3
hello world
I love ACM
good luck

输出样例
1: hello world
2: I love ACM
3: good luck

这题就是专门练那个经典 bugcin >> n 之后直接 getline 会读到空行。看你怎么处理。


AC第 3 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    int n;
    cin >> n;

    string line;
    for (int i = 1; i <= n; i++) {
        getline(cin >> ws, line);    // >> ws 吃掉残留换行,护身符
        cout << i << ": " << line << "\n";
    }
    return 0;
}

如果你没加 >> ws,第一次 getline 会读到 cin >> n 留下的空行,输出就会错位成空行 + 少一行。这就是 0.1 里反复强调的那个坑。

第 4 题:打印数字金字塔(练 string(n, ch) 重复输出)

题意:输入一个整数 n,输出一个高度为 n 的居中星号金字塔。第 i 行有 i 个星号,左边用空格补齐使其居中(第 n 行顶格)。

sample.txt
输入样例
4

输出样例
   *
  **
 ***
****

提示:第 i 行(i 从 1 到 n)= (n-i) 个空格 + i 个星号。用 string 重复构造,别手写循环拼。


下面是提示和参考答案,自己写完再看

AC第 4 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cout << string(n - i, ' ') << string(i, '*') << "\n";
        //      ↑ (n-i)个空格        ↑ i个星号
    }
    return 0;
}

string(个数, 字符) 一行搞定重复输出,比手写两个循环干净太多。


四道题对应的考点:第1题多组/EOF,第2题浮点+溢出,第3题整行读入那个经典坑,第4题字符重复输出。

教练衔接coach

你可以把代码写好贴给我,我帮你看哪里有隐患(哪怕能过样例,我也会指出潜在的 WA 点)。或者全部搞定后,我们就推进到 0.2 数据类型与溢出。你定。

第〇部分 · 0.2

0.2 数据类型与溢出

数据溢出是新手 WA 的头号杀手,而且最阴险——你的算法完全正确,样例也过了,却因为某个中间结果悄悄爆了,直接 WA,还特别难查。这一节就是要让你对溢出形成警觉。

一、先把两个数字刻进脑子

整数类型你只需要记两个的范围,其余的用得极少:

类型大致范围安全记忆阈值能存几位数
int±2.1×10⁹超过 2×10⁹ 就危险约 9~10 位
long long±9.2×10¹⁸超过 9×10¹⁸ 才危险约 18~19 位

记忆口诀:int 撑到 20 亿,long long 撑到 900 亿亿。 中间还有 unsigned int(0 到 ~4.2×10⁹)和 unsigned long long(0 到 ~1.8×10¹⁹),铜牌阶段几乎用不到,知道有就行。

二、头号杀手:乘法溢出(最重要,重点看)

看这段代码,几乎每个新手都栽过:

solution.cpp
int a = 100000, b = 100000;
long long c = a * b;          // 你以为 c = 10^10,实际是错的!

a * b 等于 10¹⁰,远超 int 的 20 亿。问题在于:等号右边 a * b先用 int 算完,算的过程中就已经溢出变成垃圾值了,然后才赋给 long long,为时已晚。

根本原因(务必理解):表达式的类型由参与运算的变量决定,跟你要赋给谁没关系。两个 int 相乘,结果就是 int。

正确写法——让其中一个先变成 long long,整个表达式就会用 long long 计算:

solution.cpp
long long c = (long long)a * b;   // 把一个操作数强转,乘法就在 long long 里进行

只要强转任意一个,整条乘法就升级了,不用两个都转。这就是第一条铁律

两个 int 相乘、结果可能超过 20 亿时,必须先把至少一个强转成 long long(long long)a * b

同样的坑也出现在「int 数组求和」「累加阶乘」「面积 = 长 × 宽」等场景。判断方法:心算一下最大可能值,超过 2×10⁹ 就开 long long。 拿不准时,开 long long 几乎没有副作用(除非数组巨大、卡内存),所以——

不确定就开 long long 这是新手最划算的保险。

三、一个偷懒大招(你会在别人代码里见到)

很多竞赛选手图省事,直接在开头写:

solution.cpp
#define int long long
signed main() {     // 注意:main 必须改成 signed,不能再是 int
    // ...
    return 0;
}

这样全文的 int 都变成 long long一劳永逸躲掉所有乘法溢出。代价是:内存翻倍(开大数组时可能 MLE)、速度略慢。

我的建议:铜牌阶段题目数据不大,这招很香,可以用。但你得知道两点——① main 要改成 signed main();② 万一开了上千万的大数组导致 MLE,就老老实实手动管理类型。先用着,遇到内存问题再说。

四、取模运算(竞赛超高频,单独讲)

很多题答案极大(比如方案数),题目会要求输出「答案对 10⁹+7 取模的结果」。两个经典模数你会反复见到:

solution.cpp
const int MOD = 1e9 + 7;        // 即 1000000007,最常见
const int MOD = 998244353;      // 第二常见

取模的运算规则,加、减、乘可以随意拆,除法不行(除法要用逆元,那是 5.3 的内容,现在先不管):

solution.cpp
// 加法
(a + b) % MOD

// 乘法——注意这里藏着溢出!
(a * b) % MOD          // a、b 各自接近 1e9 时,a*b 约 1e18,超 int 但没超 long long

这里有个连环坑:模数 1e9+7 本身接近 int 上限,两个模过的数相乘,乘积约 10¹⁸,必须用 long long 存中间结果,否则乘法那一步就溢出了。所以凡是带取模的乘法,相关变量统统用 long long

负数取模(极易 WA 的坑)

C++ 里负数取模结果可能是负的(-7) % 5 在 C++ 里等于 -2,不是 3。做减法取模时,结果一旦为负,直接输出就 WA 了。

万能修正写法,记住这个固定模板:

solution.cpp
// 减法取模,防止出现负数
long long res = ((a - b) % MOD + MOD) % MOD;
//                              ↑ 先 +MOD 拉回正数,再 %MOD

+ MOD% MOD 能保证结果一定落在 [0, MOD)。这就是取模铁律

减法取模后,永远写成 ((a - b) % MOD + MOD) % MOD,自动消灭负数。

累乘要边乘边取模

n! 这种连乘,不能等乘完再取模(早就溢出了),要每乘一次就取一次模

solution.cpp
long long fact = 1;
for (int i = 1; i <= n; i++) {
    fact = fact * i % MOD;     // 每一步都取模,数值永远不会爆
}

五、配套小坑

  • scanf 读 long long 用 %lld,写成 %d 会读错(这个 0.1 提过,再强调一次,因为太常踩)。
  • 循环变量:如果循环要跑到很大的次数(比如 for (long long i = 0; i < n; i++),而 n 接近 1e18),计数器本身也得是 long long。
  • 数组下标别用 long long 当 vector[] 下标乱来,下标用 int 就够,避免不必要的麻烦。
  • 快读/位运算涉及大数时,留意有没有中间步骤偷偷溢出。

六、提交前自查清单

  • 有没有「两个 int 相乘可能超 20 亿」?→ 强转 (long long)
  • 求和 / 累乘 / 面积 / 阶乘这类,最大值估过了吗?→ 超 2×10⁹ 开 long long。
  • 带取模的乘法,变量是 long long 吗?
  • 有没有减法取模没加 +MOD,可能输出负数?
  • scanf 的 long long 是不是 %lld

一句话总结这节:心算最大值 → 超 20 亿就 long long → 乘法记得 (long long)a*b → 取模减法记得 +MOD 把这四件事变成本能,溢出类 WA 基本绝迹。

教练衔接coach

要不要我出 2~3 道专扎溢出和取模的小题(比如「求 1 到 n 的和」「n 的阶乘取模」这种,看着简单但不开 long long 必挂)练一下手感?还是直接推进 0.3 常见易错点清单


配套练习题

好,给你 4 道题,每一道都长着「人畜无害」的脸,但不开 long long 或不处理取模就必 WA。这正是溢出的阴险之处——样例小的时候全过,一交就挂。

先自己写,写完再看折叠的答案和陷阱分析。


第 1 题:区间求和(练「求和溢出」+ 估算最大值)

题意:给你一个整数 n(1 ≤ n ≤ 10⁹),求 1 + 2 + 3 + … + n 的结果。

sample.txt
输入样例
100000

输出样例
5000050000

自查:结果最大能到多少?用 int 存得下吗?(先估,再写)


AC第 1 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    long long n;            // n 本身到 1e9,int 勉强够,但读成 long long 更省心
    cin >> n;
    long long sum = n * (n + 1) / 2;   // 等差数列求和,n*(n+1) 约 1e18,必须 long long
    cout << sum << "\n";
    return 0;
}

陷阱:n = 10⁹ 时,n*(n+1) 约 10¹⁸,远超 int 的 20 亿。如果 n 是 int,n*(n+1) 在乘的瞬间就溢出了,即使 sum 是 long long 也救不回来。把 n 直接定义成 long long,整条式子就安全了。这就是「让操作数本身是 long long」的好处。

你也许会想用 for 循环累加,n=10⁹ 会跑 10 亿次直接 TLE,所以这题必须用公式。一举两得地提醒你:大循环也是坑。

第 2 题:阶乘取模(练「边乘边取模」+ 取模乘法溢出)

题意:给你一个整数 n(1 ≤ n ≤ 10⁶),求 n! mod (10⁹+7),即 1×2×3×…×n1000000007 取余。

sample.txt
输入样例
10

输出样例
3628800
sample.txt
输入样例
20

输出样例
146326063

自查fact * i 这一步,两个数都接近 10⁹ 时乘积多大?该用什么类型存?


AC第 2 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

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

    int n;
    cin >> n;
    long long fact = 1;
    for (int i = 1; i <= n; i++) {
        fact = fact * i % MOD;     // 关键:每步取模,且 fact 是 long long
    }
    cout << fact << "\n";
    return 0;
}

陷阱fact 取模后接近 10⁹,乘以 i(也可能上百万)后约 10¹⁵,超 int 没超 long long,所以 fact 必须是 long long。如果写成 int,乘的那一步直接爆。另外必须每步取模,等乘完再取模的话,10⁶! 是个天文数字,早溢出了。

第 3 题:两数之积(练「乘法溢出」最经典形态)

题意:多组数据,处理到 EOF。每行两个整数 a b(0 ≤ a, b ≤ 10⁹),输出 a × b

sample.txt
输入样例
100000 100000
1000000000 1000000000
3 4

输出样例
10000000000
1000000000000000000
12

自查ab 用什么类型读?a * b 怎么写才不会在乘的过程中溢出?(这题就是 0.2 第二节那个核心坑的裸题)


AC第 3 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

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

    long long a, b;
    while (cin >> a >> b) {
        cout << a * b << "\n";   // a、b 是 long long,乘积约 1e18,安全
    }
    return 0;
}

这里把 ab 直接读成 long long,最省事。

如果你坚持用 int 读,那就必须强转:

solution.cpp
int a, b;
while (cin >> a >> b) {
    cout << (long long)a * b << "\n";   // 必须强转,否则 int*int 先溢出
}

典型错误写法 int c = a * b; 或者 long long c = a * b;(右边还是 int 乘 int),10⁹ × 10⁹ 在乘的过程中就废了。

第 4 题:差值取模(练「负数取模」+MOD 修正)

题意:给你两个整数 ab(0 ≤ a, b ≤ 10⁹),输出 (a − b) mod (10⁹+7),要求结果是 [0, 10⁹+7) 范围内的非负整数

sample.txt
输入样例
10 3

输出样例
7
sample.txt
输入样例
3 10

输出样例
999999990

自查:第二组 3 - 10 = -7,C++ 里 (-7) % MOD 会是负数,直接输出就 WA。怎么修正成 999999990?(提示:1000000007 - 7 = 1000000000,对上了)


下面是参考答案,自己写完再对

AC第 4 题 参考
solution.cpp
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

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

    long long a, b;
    cin >> a >> b;
    long long res = ((a - b) % MOD + MOD) % MOD;   // +MOD 修正负数
    cout << res << "\n";
    return 0;
}

陷阱a - b = 3 - 10 = -7(-7) % MOD 在 C++ 里是 -7(负数),直接输出就错。(-7 + MOD) % MOD = 1000000000,正好是答案。这个 ((x) % MOD + MOD) % MOD 模板要背死,凡是取模运算里出现减法,就套上它。


这 4 题的考点串起来正好是 0.2 的核心:第1题求和溢出+大循环 TLE,第2题累乘取模,第3题乘法强转,第4题负数取模修正。

教练衔接coach

把代码写出来贴给我,我帮你逐个挑隐患(尤其看你 long long 开得对不对、取模套路对不对)。或者你都过了,我们就推进 0.3 常见易错点清单

第〇部分 · 0.3

0.3 常见易错点清单

这一节是「血泪经验合集」——这些坑你几乎一定会踩,区别只在于踩几次。提前看一遍,能帮你在赛场上把「调一小时」变成「扫一眼就发现」。

我按踩坑频率从高到低排,重点看前三个。

一、多组数据不清空(频率★★★★★,最阴险)

这是多测题的头号杀手。你写对了单组逻辑,但忘了上一组的数据残留,第二组就用着脏数据,导致第一组样例过、后面全错或者偶尔过偶尔挂

看这个典型错误:

solution.cpp
int T;
cin >> T;
while (T--) {
    int sum = 0;
    int cnt;                    // ❌ 没初始化,但这里其实还好(下面会赋值)
    static int a[100005];       // ❌ 用了上一组留下的旧数据!
    // ... 处理
}

问题场景:全局数组、计数器、标记数组、答案变量,如果在循环外定义或忘了重置,第二组就带着第一组的「记忆」。

对策——养成习惯:每组数据开头,把所有用到的东西清零。

solution.cpp
const int N = 100005;
int a[N], vis[N];
int main() {
    int T;
    cin >> T;
    while (T--) {
        // 进入每组,先清空本组要用的东西
        memset(vis, 0, sizeof(vis));   // 标记数组清零
        int sum = 0, ans = 0;          // 变量在循环内定义=自动重置
        // ... 本组逻辑
    }
}

铁律变量尽量定义在循环内部(进循环自动重新初始化);全局数组memset 或循环手动清空。

memset 用法memset(数组名, 0, sizeof(数组名)) 清零;memset(数组, 0x3f, sizeof(数组)) 赋一个很大的值(约 10⁹,常用作「无穷大」初始化最短路)。注意 memset 只能赋 0、-1、0x3f 这种按字节填充的值,不能用它把数组填成 1 或 2(会变成奇怪的数)。

清空的代价坑:如果数组开了 10⁷ 这么大,但每组实际只用前 n 个,memset 整个数组会很慢甚至 TLE。这时只清空 0n 即可:for (int i = 0; i <= n; i++) a[i] = 0;

二、数组越界(频率★★★★★,最难查)

越界的可怕在于:有时候直接崩(RE),有时候不崩但默默读到/改写了别的数据(WA),后者能让你怀疑人生。

最常见的三种越界

(a) 数组开小了:题目说 n ≤ 10⁵,你开 int a[100000],结果下标用到 a[100000] 就越界(下标 0~99999 才合法)。

对策:数组永远多开一点,养成 const int N = 1e5 + 5; 的习惯(多开 5~10 个缓冲),下标从 1 开始时尤其要多开。

(b) 下标从 1 开始却按 0 开数组:很多人习惯数据存在 a[1]a[n](方便处理),但数组只开了 a[n],访问 a[n] 就越界。

对策:下标从 1 用,就开 a[n+1] 或直接 a[N](N 是个大常数)。

(c) 循环边界写错for (int i = 0; i <= n; i++) a[i]...,如果数组只有 n 个元素(下标 0~n-1),i = n 时越界。

对策:盯紧 < 还是 <=。下标 0 开始用 i < n,下标 1 开始用 i <= n

调试技巧:如果程序莫名其妙 RE 或 WA,且找不到逻辑错误,第一反应就是怀疑越界。把数组开大一倍试试,如果问题消失,那就是越界。

三、边界条件(频率★★★★★,最容易忽略)

算法主体对了,却死在边界上。竞赛里专门有人出极端数据卡你。必查的边界:

  • n = 0 或 n = 1:循环会不会一次都不进?数组会不会访问空元素?比如求最大值,n=0 时你的 ans 初值是什么?
  • 空输入 / 空串:字符串为空时 s[0] 越界、s.size()-1 变巨大正数(0.2 提过)。
  • 最大值边界:n 取到题目上限时,会不会溢出 / 超时 / 越界。
  • 全相同 / 全为 0 / 全负数:比如求最大子段和,全负数时答案是最大的那个负数,不是 0。
  • 单个元素:只有一个数时,很多区间逻辑会出问题。

实战习惯:写完代码,手动构造几组极端数据自测——n=1n=0、全相同、最大值。这比交上去 WA 再猜强一百倍。0.6 节会专门讲怎么系统地造数据。

举个边界经典翻车:

solution.cpp
// 求 n 个数的最大值
int ans = 0;                 // ❌ 如果所有数都是负数,答案会错成 0
for (int i = 0; i < n; i++) ans = max(ans, a[i]);
solution.cpp
// ✅ 正确:初值设成第一个元素,或设成极小值
int ans = a[0];              // 或 int ans = -2e9;
for (int i = 1; i < n; i++) ans = max(ans, a[i]);

初值设 0 是个高频错误——求最大值的初值应该是「极小」,求最小值的初值应该是「极大」,别习惯性写 0。

四、忘记初始化(频率★★★★☆)

C++ 里局部变量不会自动清零,里面是随机的垃圾值。忘了赋初值,行为完全不可预测(本地能过、评测机挂,因为内存状态不同)。

solution.cpp
int sum;                     // ❌ sum 是垃圾值
for (int i = 0; i < n; i++) sum += a[i];   // 在垃圾值基础上累加
solution.cpp
int sum = 0;                 // ✅ 累加器、计数器一定要初始化

注意区别

  • 局部变量(函数内 int x;):垃圾值,必须手动初始化
  • 全局变量 / 全局数组(函数外定义):自动初始化为 0,可以不手动清——但多测时仍要每组重新清空(见第一条)。

习惯:累加器、计数器、答案、标记,定义时顺手就赋初值,int sum = 0, cnt = 0, ans = 0;

五、浮点比较(频率★★★☆☆)

0.1 提过,这里归档进清单。浮点数因为精度误差,两个「相等」的数可能差一丝丝

solution.cpp
if (a == b)                  // ❌ 浮点直接比,可能因精度误差判错
if (fabs(a - b) < 1e-9)      // ✅ 看差的绝对值是否足够小

延伸的判断:

solution.cpp
// 判断 a 是否 ≥ b(浮点)
if (a > b - 1e-9)            // 加个 eps 容差

// 判断 a 是否为 0
if (fabs(a) < 1e-9)

能用整数就别用浮点。很多看起来要小数的题,其实可以全程用整数算(比如比较 a/bc/d,交叉相乘变成 a*dc*b,避开除法),既快又没精度问题。这是个重要的「避坑思维」。

六、其他高频小坑(速览)

  • 整数除法截断5 / 2 = 2 不是 2.5。要小数就 5.0 / 2。(0.2 那道平均分题就是这个坑)
  • cin >> 读入失败后死循环:读到非法字符时 cin 会进入错误状态,后续读入全失败。一般题不会遇到,知道即可。
  • == 写成 =if (x = 5) 是赋值(永远为真),不是比较。编译器通常会警告,留意。
  • 运算符优先级:位运算 & | 优先级很低,a & 1 == 0 实际是 a & (1 == 0),要加括号 (a & 1) == 0
  • intsize_t 混用s.size() - 1 当 s 为空时变巨大正数(0.2 强调过)。

七、提交前 30 秒自查清单

按这个顺序在脑子里过一遍,能拦住绝大多数低级 WA:

  1. 多测清空了吗?(数组、标记、计数器、答案)
  2. 数组开够大了吗?+5 缓冲,下标从 1 要 n+1
  3. 边界试了吗?(n=0、n=1、全相同、最大值)
  4. 累加器/答案初始化了吗?(求 max 别初始化成 0)
  5. 该开 long long 吗?(接上节 0.2)
  6. 浮点用 eps 比了吗?

这六条 + 那张自查清单,是你未来每场比赛的「保命符」。我自己写题,交之前都会扫一遍这个清单,它能拦掉的罚时,比多会一个算法值钱得多

教练衔接coach

要不要我故意写几段藏着这些 bug 的代码,让你来找茬(专门练「看代码揪错」的能力,这正是赛场上 debug 的核心技能)?还是直接推进 0.4 评测结果解读与对策(AC/WA/TLE/MLE/RE/CE 各是什么、遇到怎么办)?


找茬练习

好,这是个极其重要的能力——赛场上 80% 的时间在 debug,能一眼揪出 bug 的人罚时少一大截。

给你 6 段代码,每段都有 1~2 个 0.3 清单里的经典 bug。你的任务:找出每段的 bug 在第几行、是什么问题、怎么改。

建议你先把每段都看完、写下你的判断,再展开对照答案。看代码找 bug 比自己写还难,慢慢来。


第 1 段:求 n 个数里的最大值

solution.cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
    int n;
    cin >> n;
    int a[n];
    for (int i = 0; i < n; i++) cin >> a[i];
    
    int maxv = 0;
    for (int i = 0; i < n; i++)
        maxv = max(maxv, a[i]);
    
    cout << maxv << "\n";
    return 0;
}

输入 3 / -5 -2 -8期望输出 -2。这段会输出什么?


AC第 1 段 答案

Bug:第 8 行 int maxv = 0; —— 求最大值初始化成 0。

输入全是负数 -5 -2 -8 时,没有任何数能比 0 大,maxv 一直是 0,输出 0(错,应为 -2)

改法:初值设成第一个元素,或设成极小值:

solution.cpp
int maxv = a[0];           // 推荐
// 或 int maxv = -2e9;

这就是清单第三条「求 max 别初始化成 0」的活例子。

(附带一提:int a[n]; 这种变长数组在竞赛里能用但不规范,n 大时会栈溢出,建议开全局大数组 int a[N];。)

第 2 段:多组数据,每组统计有几个正数

solution.cpp
#include <bits/stdc++.h>
using namespace std;
int cnt;
int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        for (int i = 0; i < n; i++) {
            int x;
            cin >> x;
            if (x > 0) cnt++;
        }
        cout << cnt << "\n";
    }
    return 0;
}

输入

sample.txt
2
3
1 -2 3
2
-1 5

期望输出 2 然后 1。这段会输出什么?


AC第 2 段 答案

Bugcnt 是全局变量(第 3 行),多组之间没有清空

第一组数完 cnt = 2,进入第二组时 cnt 还是 2,第二组又数到 1 个正数,变成 3,输出 2 然后 3(错,第二组应为 1)

这是清单第一条「多测不清空」的头号杀手,最阴险,因为第一组永远是对的。

改法:把 cnt 移到循环内部(自动重置),或每组开头清零:

solution.cpp
while (T--) {
    int cnt = 0;           // 移进来,每组自动归零
    // ...
}

第 3 段:计算 1×2×3×…×n

solution.cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
    int n;
    cin >> n;
    long long product;
    for (int i = 1; i <= n; i++) {
        product *= i;
    }
    cout << product << "\n";
    return 0;
}

输入 5期望输出 120。这段稳定输出 120 吗?


AC第 3 段 答案

Bug:第 6 行 long long product; —— 局部变量没初始化,是垃圾值。

product *= i 是在一个随机垃圾值的基础上连乘,结果完全不可预测。本地可能凑巧输出 120,评测机上大概率是乱码。这是清单第四条「忘记初始化」。

改法

solution.cpp
long long product = 1;     // 累乘的初值是 1(累加才是 0)

注意:累乘初值是 1,累加初值是 0,别搞混。

第 4 段:读 n 个数存进数组,再倒序输出

solution.cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100000;
int a[N];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = n; i >= 1; i--) cout << a[i] << " ";
    cout << "\n";
    return 0;
}

输入 n = 100000 加 10 万个数。逻辑看着对,但可能出什么事?


AC第 4 段 答案

Bug:第 3 行 const int N = 100000;,但第 6 行下标从 1 用到 n,当 n = 100000 时要访问 a[100000],而数组合法下标是 0~99999越界

这是清单第二条,而且是最隐蔽的版本——下标从 1 开始,却没给数组留够空间。越界可能 RE,也可能默默 WA。

改法:数组多开几个:

solution.cpp
const int N = 100005;      // 多开 5 个缓冲,下标从 1 用就安全了

养成 1e5 + 5 的肌肉记忆,专治这种 off-by-one。

第 5 段:判断两个分数 a/b 和 c/d 是否相等

solution.cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
    double a, b, c, d;
    cin >> a >> b >> c >> d;
    if (a / b == c / d)
        cout << "equal\n";
    else
        cout << "not equal\n";
    return 0;
}

输入 1 3 2 6(即 1/3 和 2/6,应该相等)。这段可靠吗?


AC第 5 段 答案

Bug:第 5 行用 == 直接比较两个浮点除法结果。

1.0/32.0/6 在数学上相等,但浮点运算有精度误差,二进制存储下可能差一个极小值== 判断为不等,输出 "not equal"(错)。这是清单第五条。

改法:用 eps 容差:

solution.cpp
if (fabs(a / b - c / d) < 1e-9)

更好的改法(避坑思维):分数比较交叉相乘,全程用整数,没有任何精度问题:

solution.cpp
// 若 a b c d 是整数,判断 a/b == c/d 等价于 a*d == c*b
if ((long long)a * d == (long long)c * b)   // 注意防溢出

能转整数就转整数,这是高手的习惯。

第 6 段:判断一个数是奇数还是偶数

solution.cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
    int x;
    cin >> x;
    if (x & 1 == 0)
        cout << "even\n";
    else
        cout << "odd\n";
    return 0;
}

输入 4期望输出 even。这段输出对吗?


全部看完、写下判断后,再展开对照:

AC第 6 段 答案

Bug:第 5 行 if (x & 1 == 0) —— 运算符优先级坑。

== 的优先级高于 &,所以这句实际是 x & (1 == 0),即 x & 0,结果恒为 0(假),永远走 else 输出 "odd",连偶数 4 都被判成 odd。这是清单第六条。

改法:给位运算加括号:

solution.cpp
if ((x & 1) == 0)          // 先算 x&1,再比较

凡是 & | ^ 和比较运算混在一起,一律加括号,别赌优先级。


找对了几个? 这 6 个 bug 对应清单的 6 大类:

  1. 求 max 初值为 0
  2. 多测不清空
  3. 局部变量没初始化(且累乘初值应为 1)
  4. 数组越界(下标从 1 没留空间)
  5. 浮点 == 比较
  6. 位运算优先级

如果你能一眼揪出 4 个以上,赛场 debug 能力已经不错了。没找全很正常——第 2、3、4 段最难,因为它们「能过样例」,这正是最坑的那类。

可以告诉我你找对了几个、哪几段卡住了,我针对性再补几道;或者推进 0.4 评测结果解读与对策(AC/WA/TLE/MLE/RE/CE 遇到各自怎么办)。

第〇部分 · 0.4

0.4 评测结果解读与对策

这一节是赛场救命指南。提交后那个红色或绿色的结果,不是终点,而是给你的线索。会读这些缩写、知道每种该往哪个方向查,能让你从「无头苍蝇乱改」变成「精准定位」。

先给你一张总览表,然后逐个展开对策。

缩写全称中文一句话含义
ACAccepted通过恭喜,过了 ✅
WAWrong Answer答案错误跑完了,但答案不对
TLETime Limit Exceeded超时跑太慢,超过时限
MLEMemory Limit Exceeded超内存用的内存太多
RERuntime Error运行时错误程序中途崩溃
CECompile Error编译错误没编译成功,根本没跑
PEPresentation Error格式错误答案对,但格式(空格/换行)不对
OLEOutput Limit Exceeded输出超限输出太多(通常是死循环狂打印)

下面按你最常遇到的顺序逐个讲怎么办。


一、WA(答案错误)——最常见,也最需要冷静

含义:程序正常跑完了,但输出和标准答案不一致。注意:通常判题机只告诉你 WA,不告诉你错在哪个测试点(有些平台会显示「第 3 个点 WA」)。

排查顺序(按这个来,别瞎改)

  1. 重新读题。WA 第一反应永远是「我题意理解错了吗?」——看错数据范围、漏看某个条件、输出要求理解偏了,占 WA 的一大半。把题目再读一遍,比改代码有用。
  2. 过自查清单(就是 0.3 那张):多测没清空?数组越界?边界没处理?初值错了?该开 long long 没开?浮点用 == 了?——挨个排除。
  3. 手造边界数据自测n=1n=0、全相同、全负数、最大值。你的代码大概率死在某个边界。
  4. 如果显示了「第几个点 WA」:第 1、2 个点就 WA,通常是基础逻辑或读题错;前面都过、最后几个点 WA,通常是边界 / 溢出 / 超大数据下的特殊情况
  5. 还找不到?上对拍(0.6 会专门讲):写个暴力解 + 随机数据生成器,让正确的暴力和你的代码对跑,揪出第一组答案不同的数据。这是终极武器。

WA 心态:别一看到 WA 就慌着乱改代码。先想,再改。 盲目改往往越改越错,还浪费罚时。


二、TLE(超时)——你的算法可能不对

含义:程序在规定时间内(通常 1~2 秒)没跑完。

关键认知:TLE 几乎总是意味着算法复杂度不对,而不是「写得不够优化」。靠抠常数(卡常)救回来的是少数。

排查顺序

  1. 先看复杂度(接 0.7 的内容):拿数据范围 n 反推。n=10⁵ 你写了 O(n²)(10¹⁰ 次运算)必超。正确做法是换更优的算法,比如 O(n²)→O(n log n)。这是治本。
  2. 检查有没有死循环while 条件永远不满足、for 里下标没正常递增。死循环也会 TLE(或 OLE)。
  3. 检查 I/O 慢:用了 cin/cout忘了加 sync_with_stdio(false),或者用了 endl 狂刷缓冲。大数据量下这能让你从 AC 变 TLE。回去看 0.1 的万能开头加了没。
  4. 常数优化(最后手段,治标):减少不必要的重复计算、memset 别清整个大数组、避免在循环里反复构造大对象。但如果是复杂度问题,这些都救不了。

TLE 的典型对应关系(背下来,秒判该用什么算法):
- n ≤ 10:可以 O(n!) 暴力全排列
- n ≤ 20:可以 O(2ⁿ) 状压枚举
- n ≤ 500:O(n³) 没问题(比如 Floyd)
- n ≤ 5000:O(n²) 可以
- n ≤ 10⁵:要 O(n log n) 或 O(n)
- n ≤ 10⁶:要 O(n) 或 O(n log n),且注意 I/O
- n ≤ 10⁹:O(log n) 或 O(√n),绝不能遍历

这张表是 0.7 的核心,看到数据范围就知道目标复杂度,TLE 自然少。


三、RE(运行时错误)——程序崩了

含义:程序运行到一半挂掉了。这是个好消息,因为 RE 通常有明确的物理原因,比 WA 好查。

最常见的 RE 原因(按频率)

  1. 数组越界(占绝大多数):访问了 a[-1]a[n](开小了)、下标超范围。回去看 0.3 第二条。数组开大一点经常直接解决。
  2. 除以 0a / ba % bb == 0。特别是计算里 b 可能为 0 的情况要特判。
  3. 栈溢出:递归太深(DFS 没设终止条件,或递归层数过大),或者开了巨大的局部数组int a[10000000]; 写在 main 里,应该放全局)。
  4. 空指针 / 空容器访问vector 是空的却访问 v[0]v.back();空 string 访问 s[0]
  5. map/set 等用错:一般少见。

查 RE 技巧:本地很多编译器在越界时不一定崩(行为未定义),所以本地能跑、评测 RE 很常见。-fsanitize=address 编译(后面 0.5/0.6 讲)能精准报出越界在哪行。简单粗暴的办法:把所有数组开大一倍、检查所有除法的除数、检查递归出口。


四、CE(编译错误)——根本没跑起来

含义:代码没编译通过,一个测试点都没运行。CE 不计罚时(多数赛制下),但浪费时间。

对策:CE 一定会给你编译器报错信息,照着看就行。这正是下一节 0.5「编译错误怎么读」 的全部内容,那里会详细讲怎么看懂报错。这里先记住三个最高频的低级 CE:

  • 漏分号 ;:报错通常指向下一行,记得往上一行找。
  • 变量没定义 / 拼写错error: 'xxx' was not declared
  • 括号不匹配:少了 })
教练衔接coach

本地编译再交:强烈建议你在本地(或评测平台的编译按钮)先编译一遍再提交,把 CE 消灭在本地,别浪费提交次数和时间。


五、MLE(超内存)——开太大了

含义:程序用的内存超过限制(通常 256MB)。

对策

  1. 算一下数组占多少内存。记住这个换算:1 个 int = 4 字节,所以:
  • int a[10⁶] ≈ 4 MB(没问题)
  • int a[10⁷] ≈ 40 MB(还行)
  • int a[10⁸] ≈ 400 MB(爆了
  • 二维 int a[10⁴][10⁴] = 10⁸ 个 int ≈ 400 MB(爆了,二维数组最容易 MLE)
  1. 二维数组尤其当心a[5000][5000] = 2.5×10⁷ × 4 = 100MB,接近上限。开二维前先乘一下。
  2. 别开不必要的大数组,或者用 vector 按需分配。
  3. #define int long long 会让内存翻倍(0.2 提过),大数组场景下可能正是 MLE 元凶。

六、PE(格式错误)——答案对,格式错

含义:答案内容是对的,但输出格式不符——多了/少了空格、换行,或大小写不对。不是所有平台都有 PE,很多平台对格式很宽容,把这种直接算 AC 或 WA。

对策

  • 检查行末多余空格最后是否该有换行
  • 检查输出的单词大小写Yes vs YES vs yes,题目要哪个就用哪个)。
  • 数字之间的分隔符(空格 vs 逗号)按题目要求。

实话:PE 现在比较少见,大多数现代判题机(Codeforces 等)行末空格、多余换行都能容忍。但严格的赛制(部分 ICPC 现场赛)会卡,所以输出尽量规范。遇到「我答案明明对却不过」,可以往格式上想一想。


七、OLE(输出超限)——通常是死循环

含义:输出的内容太多,超过限制。

对策99% 是死循环里疯狂 cout。检查循环终止条件。一般你会同时看到程序「跑很久」,结合 TLE 一起排查。少数情况是真的需要输出巨多内容但你写错了输出逻辑。


八、一张「看到结果 → 第一反应」速查图

把这个记牢,赛场上看到结果立刻知道往哪想:

  • WA → 先重读题,再过 0.3 自查清单,再造边界数据,最后对拍
  • TLE → 先看复杂度(数据范围反推),再查死循环,再查 I/O 加速
  • RE → 先怀疑数组越界(开大试试),再查除 0、递归深度、空容器
  • CE → 看编译器报错(0.5 详解),先找漏分号 / 拼写 / 括号
  • MLE → 算数组内存,重点查二维数组和 #define int long long
  • PE → 查空格、换行、大小写
  • OLE → 查死循环

核心心法:每种评测结果都是线索而非判决。最忌讳的是看到 WA/TLE 就慌乱地随机改代码——那叫「玄学调试」,又慢又容易越改越坏。先根据结果类型锁定排查方向,再动手,这是高手和新手在赛场上最大的差距之一。

这一节信息量大,但你不用死记,记住第八条那张「第一反应速查图」就够用,细节回头查。

下一步两个自然的方向:

  • 0.5 编译错误怎么读(紧接 CE,教你看懂英文报错,定位到行)
  • 0.6 调试与快速查错(详解「对拍」这个揪 WA 的终极武器,以及防御性写法)

你想先深入哪个?

第〇部分 · 0.5

0.5 编译错误怎么读

这一节专治「一堆英文报错糊脸上,瞬间懵掉」。好消息是:编译器其实很啰嗦地在告诉你哪行错、错什么,你只要会读前几行就行。学会之后,CE 对你来说会变成「扫一眼、改一下、5 秒解决」的小事。

一、读报错的三条总原则(最重要)

在认具体报错之前,先建立正确的读法,这比记单词更关键:

① 永远从第一条 error 开始看,别被吓到。 编译器一个错误常常引发后面一连串「连锁报错」。第一条 error 是病根,后面的多半是它的连锁反应。所以——看到几十行红字,只读最上面那条 error,改完重新编译,往往后面全消失了。

② 分清 errorwarning

  • error(错误):必须改,否则编译不过、程序跑不了。
  • warning(警告):能跑,但提醒你可能有问题。比如「变量未使用」「比较有符号和无符号数」。竞赛里 warning 一般可以忽略(但有些 warning 其实在暗示 bug,比如 = 写成 ==,值得瞄一眼)。

只有 error 会导致 CE。 满屏 warning 但没有 error,是能正常提交的。

③ 锁定行号。 报错格式基本是这样:

sample.txt
main.cpp:8:15: error: 'x' was not declared in this scope
   ↑        ↑   ↑
 文件名   行号:列号   错误类型和说明

8:15 就是第 8 行第 15 列。直接跳到第 8 行去看。列号有时不准,但行号很可靠(除了漏分号那种,见下文)。

二、最高频报错中英对照(重点背这几个)

下面是你 99% 会遇到的报错,配上中文意思怎么改


1. 变量未声明 / 拼写错误(最常见)

sample.txt
error: 'x' was not declared in this scope

中文x 在当前作用域没有声明。 原因:变量名拼错了、忘了定义、或者用了作用域外的变量(比如在 if 里定义的变量,到外面用)。 改法:检查 x 是不是拼错(cnt 写成 cont)、是不是忘了 int x;、是不是定义在了别的花括号里。


2. 缺少分号(最坑,因为报错指向下一行)

sample.txt
error: expected ';' before '...'

中文:在某处之前期望一个分号 ;原因:上一句末尾漏了分号。 关键技巧报错指向的行往往是对的,但分号要加在它的上一行末尾! 比如报错说第 9 行有问题,实际是第 8 行末尾漏了 ;。看到 expected ';'先看上一行


3. 括号不匹配

sample.txt
error: expected '}' at end of input
error: expected ')' before '...'

中文:在输入末尾期望一个 } / 在某处之前期望 )原因:少了右花括号 } 或右括号 ),或者多了。 改法:检查括号配对。养成写代码时左右括号一起打、再往里填内容的习惯,能避免大部分这类错误。编辑器的括号高亮也能帮你找。


4. 函数/类型不存在(常常是没 include 或拼错)

sample.txt
error: 'sort' was not declared in this scope
error: 'vector' was not declared in this scope

中文sort / vector 没有声明。 原因:没包含对应头文件(比如用 sort 没写 <algorithm>)。 改法:用 #include <bits/stdc++.h> 一次性解决(这就是 0.1 让你用万能头的原因之一),就基本不会再有这类错。如果还报,检查是不是漏了 using namespace std;


5. 类型不匹配

sample.txt
error: cannot convert 'std::string' to 'int' in initialization
error: invalid conversion from 'int' to 'char*'

中文:无法把 string 转成 int / 无法把 int 转成 char*。 原因:把不兼容的类型互相赋值,比如想把字符串直接当数字用。 改法:检查变量类型,该转换的用 stoito_string 等正确转换。


6. 重复定义

sample.txt
error: redefinition of 'x'
error: redeclaration of 'int x'

中文x 重复定义了。 原因:同一个作用域里定义了两次同名变量,比如 int x; 写了两遍。 改法:删掉重复的定义,或改名。


7. 赋值给常量 / 左值错误

sample.txt
error: lvalue required as left operand of assignment

中文:赋值号左边需要一个可赋值的对象(左值)。 原因:通常是 == 写成了 = 的反面——把表达式放在了赋值号左边,或者给常量赋值。 改法:检查赋值语句,左边必须是变量。


8. main 函数相关

sample.txt
error: 'main' must return 'int'

中文:main 必须返回 int。 原因:用了 #define int long long 之后 main 还写成 int main(这时 int 被替换成 long long,类型就错了)。 改法:配合 #define int long long 时,main 要写成 signed main()(0.2 提过这个坑)。


三、warning 里值得瞄一眼的(暗示 bug)

虽然 warning 不影响编译,但这两个其实在帮你抓 bug:

sample.txt
warning: suggest parentheses around assignment used as truth value

含义:你在 if 里用了 =(赋值)而不是 ==(比较)。比如 if (x = 5)。这几乎一定是 bug!(0.3 清单提过)

sample.txt
warning: comparison between signed and unsigned integer expressions

含义:有符号和无符号数比较(常见于 int is.size() 比较)。多数时候没事,但极端情况会出 bug(0.2 那个 s.size()-1 空串变巨大正数的坑)。

四、实战:拿到一屏报错,怎么操作

给你一套固定流程,照做就行:

  1. 滚到最上面,找第一条 error(忽略 warning,忽略后面的连锁 error)。
  2. 看行号,跳到那一行。
  3. 读错误类型,对照上面的表(或看关键词:not declared=没定义/拼错,expected ';'=上一行漏分号,expected '}'=括号不配对)。
  4. 改掉,重新编译
  5. 如果还有 error,重复 1~4。一次只改一个,别一口气猜着改一堆

黄金心法改一个,编译一次。 新手最容易犯的错是看到 5 个 error 就同时猜着改 5 处,结果改出新错误,更乱。逐个击破最快。

五、本地编译命令(强烈建议配一个)

如果你在本地写代码,用这条命令编译能得到清晰的报错,还能开额外检查:

sample.txt
g++ -O2 -Wall -fsanitize=address main.cpp -o main

参数含义:

  • -O2:开启优化(让你的程序更快,接近评测机环境)。
  • -Wall:开启所有警告(warning),帮你提前发现潜在 bug,比如那个 =/== 的坑。
  • -fsanitize=address运行时检测数组越界、内存错误,会直接告诉你越界发生在哪一行——这是查 RE 的神器(0.4 RE 那节提过)。

平时调试就用这条,本地能编译 + 能跑过自测,再提交,CE 和很多 RE 都能消灭在本地。

如果你用 Dev-C++、VS Code、CLion 之类的 IDE,编译报错会直接显示在下方面板,双击报错就能跳到对应行,更方便。原理一样:看第一条 error、看行号、对照含义


六、本节速记卡

赛场上看到 CE,脑子里就过这几条:

  • 满屏红字 → 只看第一条 error,后面多是连锁反应
  • not declared in this scope → 变量拼错忘定义
  • expected ';'上一行末尾漏分号
  • expected '}' / ')'括号不配对
  • 'xxx' was not declared(sort/vector 等) → 用万能头 <bits/stdc++.h> + using namespace std;
  • main must return int → 配 #define int long long 要用 signed main()
  • 原则:改一个,编译一次

会读报错之后,CE 真的就是个小事了。绝大多数 CE 就是漏分号、拼写错、括号不配对这三样,对照行号几秒钟就能解决。

下一节 0.6 调试与快速查错 是这一系列工程技巧的压轴重点——重点讲「对拍」:怎么用一个暴力解 + 随机数据生成器,自动揪出你代码 WA 的那组数据。这是从「靠运气过题」进化到「主动找 bug」的关键一步。要现在开始吗?

第〇部分 · 0.6

0.6 调试与快速查错

这是第〇部分实战技巧的压轴重点。前面教你「怎么读评测结果的线索」,这一节教你主动出击、自己把 bug 揪出来——尤其是「对拍」这个武器,能让你在没有标准答案的情况下,自己造出让代码出错的数据。掌握它,你查 WA 的能力会上一个台阶。

我按从易到难、从日常到终极的顺序讲。

一、防御性写法(治未病,写的时候就少埋雷)

最好的调试是不产生 bug。这些习惯能从源头堵住 0.3 清单里的大部分坑:

① 数组习惯性多开、用大常数。

solution.cpp
const int N = 1e5 + 5;     // 固定写法,多开 5 个缓冲
int a[N], vis[N];          // 全局定义,自动初始化为 0

下标从 1 用就不会越界,全局数组还自动清零,一举两得。

② 累加器/答案定义即初始化。

solution.cpp
int sum = 0, cnt = 0;
int ans = 0;               // 求和类
int maxv = -2e9, minv = 2e9;   // 求极值类,初值给极端

③ 多测在循环开头统一清空。

solution.cpp
while (T--) {
    memset(vis, 0, sizeof(vis));   // 或只清前 n 个
    int ans = 0;                    // 局部变量自动重置
    // ...
}

④ 复杂表达式拆开 + 加括号。 别把一长串运算挤一行,拆成几步更好查,位运算/比较一律加括号(防 0.3 那个优先级坑)。

⑤ 写完先估复杂度、估内存、估溢出。 提交前花 10 秒:数据范围反推复杂度会不会 TLE?数组多大会不会 MLE?有没有乘法要开 long long?这三件事想清楚,省下大把罚时。

防御性写法的核心思想:把容易错的地方变成固定的肌肉记忆,这样你写代码时根本不用思考就是对的,脑子专注于算法。

二、构造边界数据自测(提交前的必修课)

别一写完就提交。先自己造几组极端数据喂给程序,很多 bug 当场现形。必造的几类:

  • 最小n = 0n = 1、空串。
  • 全相同:所有数一样,比如 5 5 5 5
  • 全极端:全是负数、全是 0、全是最大值。
  • 最大规模:n 取到题目上限,看会不会 TLE / MLE / 溢出。
  • 特殊结构:已经排好序的、完全逆序的、只有两个元素的。

举例:题目说 1 ≤ n ≤ 10⁵,你就手动测 n=1(边界)、n=2(最小有效区间)、再随便造个 n=10⁵ 的大数据(用下面的生成器)测速度。这一步能拦掉一半 WA。

三、对拍(终极武器,重点中的重点)

什么是对拍

当你的代码 WA 了,但死活找不到错在哪、也没有标准答案对照时,对拍是救命稻草。

核心思想:

写一个一定正确但很慢的暴力解,再写一个随机数据生成器,让暴力解和你的「正解」对成千上万组随机数据,一旦发现某组数据两者答案不同——这组数据就是让你 WA 的反例,而且数据通常很小,方便你手动调试。

为什么有效:暴力解逻辑简单,几乎不会写错(比如全排列枚举、O(n²) 暴力),它的答案可信。你的正解快但可能有 bug。两者比对,分歧点就是 bug 所在。

对拍需要四个文件

  1. std.cpp——暴力解(慢但保证对)。
  2. my.cpp——你的正解(快但怀疑有 bug)。
  3. gen.cpp(或脚本)——随机数据生成器。
  4. 对拍脚本——自动反复生成数据、跑两个程序、比对结果。

第一步:写暴力解 std.cpp

用最朴素、最不会错的方法解同一题。比如「求区间和」,正解用前缀和 O(1),暴力就老老实实 for 循环累加 O(n);「求最优方案」,正解用 DP,暴力就全排列枚举所有可能。慢没关系,对就行。

第二步:写数据生成器 gen.cpp

生成符合题目格式的随机小数据(注意:对拍用的数据要,比如 n 只取 1~10,这样暴力跑得动,反例也好读)。

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

int main(int argc, char* argv[]) {
    srand(atoi(argv[1]));         // 用命令行参数做随机种子,保证每次不同
    int n = rand() % 10 + 1;       // n 在 1~10
    cout << n << "\n";
    for (int i = 0; i < n; i++) {
        cout << rand() % 100 << " ";   // 每个数 0~99
    }
    cout << "\n";
    return 0;
}

关键点:argv[1] 当随机种子(对拍脚本每次传不同的数进来),否则每次生成的数据都一样,失去意义。

第三步:对拍脚本

Windows 用 .bat,新建 duipai.bat

sample.txt
@echo off
:loop
    gen.exe %random% > in.txt
    my.exe < in.txt > my_out.txt
    std.exe < in.txt > std_out.txt
    fc my_out.txt std_out.txt > nul
    if errorlevel 1 goto fail
    echo AC
    goto loop
:fail
    echo WA found! 看 in.txt
    pause

fc 是 Windows 的文件比较命令,%random% 是系统随机数(当种子)。一旦两个输出不同(fc 报错),停下来,in.txt 里就是反例。

Linux/Mac 用 .sh,新建 duipai.sh

sample.txt
#!/bin/bash
for ((i = 1; ; i++)); do
    ./gen $i > in.txt
    ./my < in.txt > my_out.txt
    ./std < in.txt > std_out.txt
    if ! diff my_out.txt std_out.txt > /dev/null; then
        echo "WA found on test $i! 看 in.txt"
        break
    fi
    echo "AC on test $i"
done

diff 是 Linux 的文件比较命令。运行前先 chmod +x duipai.sh,再 ./duipai.sh

第四步:编译运行

sample.txt
# 先把三个程序都编译好
g++ gen.cpp -o gen
g++ my.cpp -o my
g++ std.cpp -o std

# 然后跑对拍脚本
./duipai.sh          # Linux/Mac
# 或 duipai.bat      # Windows

脚本会一直跑,刷出一串 AC,直到撞见一组 WA 就停下。打开 in.txt这就是让你代码出错的最小反例,拿它去手动调试你的 my.cpp,问题一目了然。

对拍的威力:很多你肉眼绝对找不到的边界 bug(某种特殊排列、某个临界值),对拍跑几秒钟就能自动撞出来。这是 ACM 选手的标配技能,一定要亲手搭一次,搭过一次以后就是复制粘贴改改的事。

对拍的适用与局限

  • 适用:答案唯一的题(数值、计数、判定)。
  • 不适用 / 需变通:答案不唯一的题(比如「输出任意一组合法方案」),这时暴力和正解可能都对但输出不同,要写个 checker(校验器)判断你的输出是否合法,而不是直接比对。这个铜牌阶段少见,先不深究。

四、二分定位 bug(缩小排查范围)

当一段代码很长、不知道哪里开始出错时,可以用「二分」的思想快速缩小范围:

① 注释二分法:把代码从中间砍开,先只跑前半段、输出中间结果,看中间值对不对。对,说明 bug 在后半;不对,bug 在前半。再对出问题的那半继续砍。几次就能定位到具体哪几行。

② 输出中间变量(最常用、最朴素):在关键步骤插 cout 打印中间值,看到哪一步的值开始不对,bug 就在那附近。

solution.cpp
cout << "DEBUG: i=" << i << " sum=" << sum << "\n";   // 临时调试输出

提示:调试用的 cout 记得提交前删掉或注释,否则会污染输出导致 WA / PE。有个常用技巧是用宏控制:

solution.cpp
#define debug(x) cerr << #x << " = " << x << "\n"

cerr(标准错误流)输出调试信息,它不会进入提交的答案(判题机只看标准输出 stdout),本地却能看到。debug(sum) 会打印 sum = 42。这个小技巧很实用。

五、善用 sanitizer 抓越界/内存错误(查 RE 神器)

接 0.5 提过的,本地编译加上这个参数:

sample.txt
g++ -fsanitize=address -g my.cpp -o my

然后正常运行,一旦发生数组越界、访问非法内存,它会直接打印出错在哪一行、什么类型的错误。查 RE 和隐蔽越界(那种不崩但 WA 的)特别好用。本地跑一遍带 sanitizer 的版本,很多隐患当场暴露。

六、查错流程总结(把前面串起来)

遇到 WA/RE 找不到原因,按这个漏斗式顺序走:

  1. 重读题 → 确认没理解错(最高频,先做这个)。
  2. 过 0.3 自查清单 → 越界?没清空?初值?溢出?
  3. 造边界数据自测 → n=0/1、全相同、最大值。
  4. cerr 打印中间变量 / 二分注释定位 → 缩小到具体哪几行。
  5. -fsanitize=address → 抓越界/内存错误(针对 RE)。
  6. 上对拍 → 前面都没招时的终极武器,自动揪反例。

越往后越重,但越往后越能解决「肉眼找不到」的硬骨头。日常小 bug 走到第 3 步基本就解决了,对拍是留给真正难缠的 WA 的。


这一节的灵魂:调试不是「盯着代码空想」,而是有方法、有工具、有流程地主动逼出 bug。新手 debug 靠运气和直觉,高手 debug 靠对拍、sanitizer 和系统化的排查顺序。你把「对拍」亲手搭一次、把 cerr 调试和 sanitizer 用熟,赛场上的查错效率会和过去判若两人。

到这里,第〇部分还剩两块

  • 0.7 时间空间复杂度分析——其实前面 TLE 那节已经剧透了核心(数据范围反推复杂度那张表),这节会系统讲怎么计算和估算复杂度。
  • 0.8 偷分 / 拿部分分策略——做不出整题时怎么捞分(暴力、打表、特判、骗分)。

还有你最初提到的 0.9 比赛策略(读题顺序、开题取舍、罚时管理)。

建议接着讲 0.7,把复杂度这个贯穿始终的核心彻底打通。要继续吗?

第〇部分 · 0.7

0.7 时间空间复杂度分析

这一节是整个竞赛的导航仪。前面 TLE 那节已经剧透了核心思想,这里系统打通。学会之后,你拿到一道题,看一眼数据范围就知道该用什么复杂度的算法,相当于排除了 90% 的错误方向——这是新手和老手最大的思维差距之一。

一、先建立一个核心数字感

评测机 1 秒大约能跑 10⁸(一亿)次基本运算。这是你所有估算的基准。

记住:时限内总运算量控制在 10⁸ 量级以内就安全,超过 10⁹ 基本 TLE。

「基本运算」指一次加减乘、一次比较、一次数组访问。常数大的操作(取模、开根、STL 容器操作)会慢几倍,所以 10⁸ 是个留了余量的安全线,实际能不能过还要看常数,但用它估算八九不离十。

二、复杂度是什么(30 秒理解)

时间复杂度 = 运算次数随数据规模 n 增长的趋势,用大 O 表示,只看最高次项、忽略常数。

  • O(1):运算次数固定,和 n 无关(比如 a[i])。
  • O(n):跑一遍,n 次(一层循环)。
  • O(n²):双重循环,n×n 次。
  • O(n log n):排序、很多分治算法。
  • O(2ⁿ)、O(n!):指数/阶乘级,爆炸增长,只能用于很小的 n。

怎么数复杂度:看循环嵌套层数。一层循环 O(n),两层嵌套 O(n²),三层 O(n³)。循环里调用了一个 O(n) 的操作(比如 find),复杂度要再乘一层。

solution.cpp
for (int i = 0; i < n; i++)          // O(n)
    for (int j = 0; j < n; j++)      // 再套一层 → O(n²)
        sum += a[i] * a[j];

三、核心技能:从数据范围反推算法(重点背)

这是本节的灵魂。拿到题先看 n 的范围,反推目标复杂度,再去想对应算法。 把这张表刻进脑子:

n 的范围目标复杂度可用的算法方向
n ≤ 10~11O(n!)全排列暴力枚举
n ≤ 20~25O(2ⁿ)状压枚举、子集枚举、爆搜
n ≤ 100O(n³)Floyd、区间 DP、三重循环
n ≤ 500O(n³)同上,n³≈1.25×10⁸ 临界但常能过
n ≤ 5000O(n²)普通 DP、双重循环、O(n²) 暴力
n ≤ 10⁵O(n log n)排序、二分、堆、树状数组、线段树
n ≤ 10⁶O(n) 或 O(n log n)线性扫描、前缀和、双指针,注意 I/O 加速
n ≤ 10⁷~10⁸O(n)只能线性,且常数要小
n ≤ 10⁹ 及以上O(log n) 或 O(√n)二分、快速幂、数学公式、根号算法,绝不能遍历

怎么用这张表(实战思维)

  • 看到 n ≤ 10⁵,心里立刻想:「O(n²) 是 10¹⁰,超了,必须 O(n log n)」→ 于是往排序、二分、数据结构方向想,直接排除掉 O(n²) 的暴力思路
  • 看到 n ≤ 20,立刻想:「这么小,大概率状压或爆搜」→ 2ⁿ = 10⁶,完全跑得动。
  • 看到 n ≤ 10⁹,立刻想:「绝对不能循环遍历 n,要么数学公式 O(1),要么二分 O(log n),要么根号 O(√n)」。

这就是逆向思维:不是「我想到一个算法,试试能不能过」,而是「数据范围告诉我必须多快,我去找这个速度的算法」。数据范围是出题人给的最大暗示,等于半个题解。

四、几个典型数字的直觉

帮你建立量级感,常见 n 对应的运算量:

  • n = 10⁵,O(n²) = 10¹⁰ → 超时 100 倍,想都别想
  • n = 10⁵,O(n log n) ≈ 10⁵ × 17 ≈ 1.7×10⁶ → 飞快
  • n = 10⁶,O(n) = 10⁶ → 轻松
  • n = 10⁶,O(n log n) ≈ 2×10⁷ → 没问题
  • n = 40,O(2ⁿ) = 10¹² → 爆炸!(这时要用「折半搜索」把 2⁴⁰ 降到 2²⁰)
  • n = 10⁹,O(√n) ≈ 3×10⁴ → 瞬间完成

log 的直觉:log₂(10⁶) ≈ 20,log₂(10⁹) ≈ 30。所以乘一个 log 大约就是乘 20~30,对 10⁵、10⁶ 这种规模几乎可以忽略,这就是为什么 O(n log n) 和 O(n) 实战中差别不大。

五、空间复杂度(别忽略,MLE 也是 0 分)

接 0.4 MLE 那节,这里归到复杂度框架里。内存换算记牢

1 个 int = 4 字节,1 MB ≈ 2.5×10⁵ 个 int。常见内存限制 256MB ≈ 6×10⁷ 个 int。

所以:

  • 一维 int a[10⁷] ≈ 40MB ✅
  • 一维 int a[10⁸] ≈ 400MB ❌ 爆
  • 二维 int dp[5000][5000] = 2.5×10⁷ 个 int ≈ 100MB ⚠️ 接近上限,再大就爆

二维数组是 MLE 的头号来源,开之前一定乘一下两个维度。比如 dp[10⁴][10⁴] = 10⁸ 个 int = 400MB,直接爆。这种情况常常需要用「滚动数组」把第一维压成 2(DP 优化技巧,后面 DP 专题会讲)。

六、实战中的注意点

① 常数因子有时致命。 大 O 忽略常数,但实战里常数会影响成败。同样 O(n):

  • cin/cout 不加速 vs 加速,差好几倍。
  • 频繁取模、开方、用 map(带 log 且常数大)会拖慢。
  • n ≤ 10⁶ 且 O(n) 时,I/O 不加速可能就 TLE,这就是 0.1 那个万能开头的意义。

② STL 容器的隐藏复杂度。

  • map / set 的增删查都是 O(log n),不是 O(1)!循环里用 map 会多乘个 log。
  • unordered_map 平均 O(1) 但常数大、最坏会退化,铜牌阶段够用但心里有数。
  • vectorpush_back 均摊 O(1),但在循环里反复 v.insert(开头) 是 O(n),容易写出隐藏的 O(n²)。

③ 多组数据要乘组数。 如果有 T 组数据,总复杂度是 T × 单组复杂度。单组 O(n) 看着没事,但 T=10⁴、n=10⁴ 时总量 10⁸,到临界了。多测时一定把组数算进去。

七、一个完整的实战推演

给你演示拿到题怎么用复杂度思考。假设题目:「给 n 个数(n ≤ 10⁵),问有多少对数之和等于 k」。

思考过程

  1. 看数据范围 n ≤ 10⁵ → 目标 O(n log n) 或 O(n),O(n²) 的暴力(枚举所有对,10¹⁰)排除
  2. O(n²) 是什么?两层循环枚举每一对 → 太慢。
  3. 怎么降到 O(n log n)?排序 + 双指针,或者用哈希表 O(n):遍历每个数 x,查 k-x 出现过几次。
  4. 选哈希表方案,O(n),稳过。

看,数据范围在第 1 步就帮你否决了最容易想到的暴力,逼你往更优的方向想。这就是复杂度分析的实战价值——它不是做完题验算用的,而是开题时就该用的导航。

八、本节速记卡

  • 基准:1 秒 ≈ 10⁸ 次运算,总量压在 10⁸ 内。
  • 核心:看 n 范围 → 反推目标复杂度 → 找对应算法。把第三节那张表背熟。
  • 快速对照:n≤20 爆搜/状压,n≤500 三次方,n≤5000 平方,n≤10⁵ 带 log,n≤10⁶ 线性,n≤10⁹ 根号/log。
  • 空间:256MB ≈ 6×10⁷ 个 int,二维数组开前先乘
  • 陷阱:常数(I/O、取模)、STL 的 log、多测乘组数。

把第三节那张「数据范围 → 复杂度」的表背到条件反射,是你这两个月最划算的投资之一。它让你在动手前就走对方向,避免把时间浪费在注定 TLE 的思路上——而方向选对,是铜牌选手最重要的能力。

第〇部分还剩两块收尾:

  • 0.8 偷分 / 拿部分分策略——题目做不出整体,怎么用暴力/打表/特判捞分。
  • 0.9 比赛策略——读题顺序、开题取舍、卡题止损、罚时管理、队内配合。
教练衔接coach

这两块偏「战术」,篇幅比前面短。要不要一起讲完,给第〇部分收个尾?

第〇部分 · 0.8

0.8 偷分 / 拿部分分策略

这一节讲的是竞赛功利学——比赛的目标是「最大化得分」,不是「优雅地完整解题」。当你正解想不出来,或者时间不够时,捞一部分分往往就是铜牌和铁牌的差距。这是一种重要的比赛意识,很多有实力的新手吃亏就吃在「非黑即白」:要么完美 AC,要么交白卷。

不过先说清楚一个关键前提,它决定了偷分策略怎么用:

〇、先搞清楚你打的是哪种赛制(极其重要)

偷分策略完全取决于评测方式,这点必须先分清:

① ICPC / ACM 赛制(区域赛是这种)——「全 or 无」

  • 一道题要么全部测试点通过(AC),要么不得分。没有「过一半给一半分」这回事。
  • 罚时机制:每道 AC 的题,从开赛到通过的时间计入罚时;每次提交 WA 额外加 20 分钟罚时(仅对最终 AC 的题生效)。
  • 排名:先比 AC 题数,题数相同比罚时

这对偷分的含义:在 ICPC 赛制下,「拿部分分」不是指拿一道题的部分分(不存在),而是指——在有限时间里,多 AC 几道简单题、别在难题上耗死。所谓「偷分」更多是指用暴力/打表等不那么聪明的方法,把一道你不会正解但数据弱的题整道骗过去拿到 AC

② OI / IOI 赛制(部分校赛、洛谷比赛是这种)——「按测试点给分」

  • 一道题分成若干 Subtask(子任务) 或测试点,过几个点拿几个点的分
  • 题目常常明示:「对于 30% 的数据,n ≤ 100;对于 100% 的数据,n ≤ 10⁵」。

这对偷分的含义:OI 赛制下偷分空间巨大!正解不会,就写暴力骗那 30%~60% 的小数据分,是标准操作,性价比极高。

你的目标是区域赛(ICPC),所以下面的策略会标注适用赛制。 但 OI 思路你也要懂,因为平时训练(洛谷等)大量是 OI 赛制,且这种「分层骗分」思维对 ICPC 选题也有启发。


一、暴力大法(最常用,两种赛制都管用)

核心:放弃高效正解,用最朴素、最直接的方法把题做出来。

ICPC 赛制下——如果一道题数据范围其实不大(比如 n ≤ 1000),那「暴力」就是正解!很多新手被难题表象吓到,没注意到数据范围允许暴力。

  • 看到 n ≤ 20,别想高级算法,直接 2ⁿ 爆搜就过了。
  • 看到 n ≤ 100,O(n³) 都能过,三重循环硬莽
  • 这其实是 0.7 复杂度反推的应用:数据范围小 = 出题人允许你暴力

OI 赛制下——正解是 O(n log n) 但你只会 O(n²),那就写 O(n²),骗到「n ≤ 5000」那档的部分分

铁律任何题,先写出能想到的最朴素暴力解。哪怕只能过小数据,在 OI 赛制是实打实的分;在 ICPC 赛制,万一数据范围允许,它就是 AC。永远不要因为「这方法太笨」就不写。

二、打表(特定场景的杀招)

核心:把答案提前算好,直接存进代码里输出,绕过运行时计算。

两种打表方式

① 本地预处理打表(OI 赛制神器): 有些题答案需要超长时间计算,但输入范围有限。你可以在本地慢慢把所有可能的答案算出来,存成一个数组,直接写进提交代码。

solution.cpp
// 比如答案只和 n(1~50)有关,本地算好存数组
int ans[] = {0, 1, 1, 2, 3, 5, 8, ...};   // 直接查表输出
cout << ans[n] << "\n";

适用:打表找规律题、答案空间小但单次计算慢的题。

② 找规律打表(两种赛制都可能用): 当你看不出数学规律时,写个暴力程序把前几项答案打印出来,人工观察规律:

solution.cpp
// 暴力跑出 n=1~15 的答案,肉眼找规律
for (int n = 1; n <= 15; n++)
    cout << n << ": " << brute_force(n) << "\n";
// 可能发现答案是斐波那契、是 2^n-1、是 n(n+1)/2 之类

看出规律后,直接用公式 O(1) 解决。这是数学题、找规律题的常用破解手段,先暴力打表 → 观察 → 猜公式 → 验证。

打表在 ICPC 赛制偶尔有用(找规律),在 OI 赛制威力更大(直接存答案骗分)。OEIS 网站(整数数列百科)也能帮你根据前几项查出数列规律,是找规律利器。

三、特判(边界和特殊情况捞分)

核心:对特殊的、简单的输入单独写处理逻辑,先把这部分分拿到手。

典型场景

  • 特判 n=1、n=2 这种小情况,往往答案显而易见,单独 if 处理。
  • 特判全相同、全 0、单调 等特殊结构,可能有简单规律。
  • OI 赛制里,题目给的 Subtask 经常就是「特殊性质」档,比如「保证图是一棵树」「保证所有数相等」,针对这些特殊性质写专门代码骗分。
solution.cpp
if (n == 1) { cout << a[0] << "\n"; return 0; }   // 特判小情况
if (所有数都相等) { /* 简单逻辑 */ }                 // 特判特殊结构
// 一般情况...

特判的价值:很多题的一般情况很难,但某些特殊情况是送分的。先把送分的特判掉,至少不空手。在 OI 赛制这直接换分,在 ICPC 赛制这是保证你正解代码在边界不挂的必要手段。

四、贪心猜结论(高风险高回报)

核心:很多题(尤其 Codeforces 风格)的正解是一个简洁的贪心结论或数学结论,证明很难但结论很短。比赛时——大胆猜结论,用小数据/样例验证,对了就交。

实战做法

  1. 观察样例,猜一个贪心策略(排序后取两端?优先选最大的?配对?)。
  2. 不急着证明(赛场上证明太费时),先用暴力对拍(0.6 的对拍!)验证你的猜想在小数据上对不对。
  3. 对拍跑几千组都一致 → 大概率是对的,直接交

这是 Codeforces 上分的核心套路之一:猜 + 对拍验证 > 严格证明。很多 Div2 的 A、B、C 题,结论一两行,但你要是非得证出来才敢写,时间就没了。对拍就是你「不证而信」的底气——这也是为什么 0.6 要你务必学会对拍。

风险提示:猜错了会 WA + 罚时。所以一定先对拍验证再交,别凭感觉裸交。

五、骗分(兜底,纯捞分)

核心:实在不会,输出一个可能蒙对的答案,万一中了就是白赚。主要用于 OI 赛制(有部分分),ICPC 赛制意义不大(要么全对要么没分,骗中整道题的概率极低)。

OI 赛制常见骗分手法

  • 输出 0 / 输出固定值:有些计数题、判定题,答案是 0 或 "NO" 的测试点占一定比例,全输出 0 可能白嫖几个点。
  • 输出样例答案:极端情况下,把样例输出直接打印,万一有个测试点就是样例……(聊胜于无)。
  • 随机输出 / 输出输入本身:某些构造题,输出输入原样可能恰好合法。
  • 贪心兜底:想不出正解,用一个看起来合理的贪心,能骗多少点是多少点。

骗分是最后的兜底,心态是「反正不会,扔个答案碰碰运气,不亏」。在 ICPC 赛制,与其骗分,不如把时间投到另一道有把握的简单题上。


六、ICPC 赛制下的「偷分」真谛(敲黑板)

因为你打区域赛,这段最重要。ICPC 没有部分分,所以「偷分」的真正含义是战略层面的得分最大化

  1. 快速扫一遍所有题,挑最简单的先做(队伍要分工读题,0.9 会讲)。铜牌靠的是把签到题和稍简单的题又快又稳地 AC,不是死磕难题。
  2. 数据范围小的题,用暴力骗过整道——这是 ICPC 最实际的「偷分」。比如一道题看着要高级算法,但 n ≤ 100,你用 O(n³) 暴力直接 AC,省下想正解的时间。
  3. 会做的题别挂罚时:先在本地/草稿想清楚边界、造好数据,有把握再交,避免反复 WA 累积罚时。罚时管理本身就是「省分」。
  4. 果断弃题止损:一道题卡 40 分钟没思路,立刻换题,别让一道难题拖垮整场。会放弃,也是一种得分策略。

一句话:ICPC 的「偷分」= 选对题 + 暴力莽下数据弱的题 + 不挂罚时 + 及时弃题。 这比任何花哨技巧都重要,是铜牌的核心打法。


七、本节速记卡

  • 先认赛制:ICPC(全或无,你的目标)vs OI(按点给分)。策略完全不同。
  • 暴力:永远先写朴素解。数据范围小(n≤100/1000)时,暴力就是正解。
  • 打表:本地算好存数组(OI);暴力打印前几项找规律(通用)。
  • 特判:n=1、全相同等特殊情况单独处理,先拿送分点。
  • 猜结论:大胆猜贪心/数学结论,对拍验证后再交(不证而信的底气来自对拍)。
  • 骗分:OI 兜底捞分;ICPC 意义不大,不如转战其他题。
  • ICPC 偷分真谛:选简单题、暴力莽弱数据题、控罚时、及时弃题。

偷分策略的本质是比赛思维的成熟:你追求的是这场比赛的总分最高,而不是某一道题的完美。一个会暴力捞分、会及时弃题、会控制罚时的选手,比一个只会死磕正解的选手,排名往往更高。 这种功利但理性的心态,正是从「会写代码」到「会打比赛」的跨越。

第〇部分就剩最后一块了——0.9 比赛策略(读题顺序、开题取舍、卡题止损、罚时管理、队内三人配合),它会把前面所有工程技巧整合成一套完整的临场作战流程。讲完这块,咱们这套「实战工程技巧」就圆满收官,可以进入第一部分的算法正题了。要收尾吗?

第〇部分 · 0.9

0.9 比赛策略

这是第〇部分的收官,也是把前面所有技巧整合成临场作战流程的一节。前面教的是「单兵作战能力」,这一节教的是「怎么打一整场比赛」——同样的实力,会打和不会打,名次能差出一大截。ICPC 是三人一队、共用一台电脑、5 小时的团队赛,策略尤其重要。

一、ICPC 比赛的基本盘(先明确规则)

  • 三人一队,一台电脑,5 小时,通常 10~13 道题。
  • 排名:先比 AC 题数,题数相同比总罚时(罚时越少越靠前)。
  • 罚时计算:每道 AC 的题 = 通过时刻(从开赛算起的分钟数)+ 该题之前每次 WA × 20 分钟。没 AC 的题不计罚时。
  • 铜牌定位:通常是中下游档位,稳定 AC 掉那几道签到题 + 1~3 道稍难的题,控制好罚时,就能摸到铜牌线。铜牌不需要你做出压轴难题。

记住这个赛制特点,下面所有策略都是围绕「多过题 + 少罚时」这个排名规则设计的。

二、开局:抢读题,铺开战场(前 20 分钟最关键)

开局绝对不要三个人挤着看第一题。最优开局是分工读题,快速摸清全场难度分布

标准开局流程

  1. 一人立刻抢电脑,敲好万能模板(0.1 那个开头),调试好环境,准备做最快能确定的签到题。
  2. 三人分头读题:比如按题号 A-B-C-D 分给三人,或者从两端往中间读,目标是 20 分钟内把所有题大致扫一遍
  3. 边读边给题目「贴标签」:每道题在心里/草稿纸上标记——
  • 签到题(一眼会,立刻能写)
  • 🟡 中等题(有思路,要点时间)
  • 🔴 难题/没思路(先放着)
  • 还要记下数据范围(决定能不能暴力,接 0.7/0.8)。
  1. 快速同步:读完碰个头,「哪些是签到、哪些有思路、哪些先放弃」,确定开题顺序

核心原则一定要先把所有题扫完再决定做什么。 最忌讳从 A 题开始一道道顺着做——万一 A 是难题,你会卡在开头,而后面 G 题可能是全场最水的签到题,被你晾着。看榜也一样(如果有实时榜),哪道题过的队多,哪道就简单,优先做。

三、做题顺序:永远先摘最低的果子

铁律:按「你能多快 AC」排序,从最有把握、最快的开始做。 不是按题号,不是按你觉得「有意思」。

  • 签到题优先,越快 AC 越省罚时(罚时按通过时刻累加,早过的题罚时少)。
  • 几道签到题先清光,把稳的分全部拿到手,再去碰中等题。
  • 中等题里,挑思路最清晰的先做,别挑最有挑战的。
  • 难题/没思路的,放到最后或直接放弃。

为什么顺序这么重要:假设两道题你都会,一道 10 分钟能写完,一道要 40 分钟。先写快的——因为罚时按 AC 时刻算。先过快的(罚时 +10),再过慢的(罚时 +50),总罚时 60;如果反过来先写慢的,两道题罚时变成 50 + 60 = 110。同样 AC 两题,顺序不同罚时差一截,直接影响排名。

四、卡题止损:会放弃比会做题更重要

新手最大的陷阱:在一道题上死磕,不肯放手,结果一道题吃掉两小时,全场崩盘。

止损硬规则(给自己定死,别凭感觉):

  • 一道题想了 30~40 分钟没有清晰思路 → 立刻换题。 把它挂起来,去做别的,说不定做别的时灵感就来了。
  • 一道题 WA 了 3~4 次还找不到原因 → 暂时放下,换个人看,或先做别的,回头再用对拍查(0.6)。别在一道题上连续撞墙,越急越错,罚时还猛涨。
  • 一道题代码写了很久还没调通 → 评估「再投入多少能过」,如果看不到头,果断弃,转向更有把握的题。

沉没成本是大敌:「我已经花了 1 小时了,不甘心放弃」——这种心态会害死你。已经花的时间收不回来,只该看「接下来怎么得分最多」。 一道卡死的难题,不如去稳稳拿下另一道签到题。会及时止损,是成熟选手的标志。

五、罚时管理:每次提交都要慎重

罚时是 ICPC 排名的第二关键因素(题数相同时决定胜负),WA 一次 +20 分钟,很贵。所以:

  • 想清楚再交,别当编译器用。 写完先在本地/草稿过一遍 0.3 自查清单、造几组边界数据自测(0.6),有把握再提交。新手爱「写完就交看看」,每次 WA +20 分钟,几次下来罚时爆炸。
  • 简单题更要一次过。 签到题如果都 WA 好几次,罚时累积 + 心态崩,得不偿失。越简单的题越要稳。
  • 交之前,队友能扫一眼最好。 团队赛的优势就是多双眼睛,关键提交前让队友瞄一下边界、读题理解对不对。
  • 本地必须编译通过再交(接 0.5),别让 CE 浪费提交(虽然 CE 多数不计罚时,但浪费时间和节奏)。

一句话:ICPC 的提交是「枪里的子弹」,不是「试错的草稿」。 每发都要瞄准了再打。

六、三人配合:一台电脑怎么协同(团队赛精髓)

一台电脑是最大的约束——同一时间只有一个人能写代码,所以配合至关重要:

① 角色大致分工(灵活,非死板):

  • 通常按擅长领域分:一人强 DP,一人强图论,一人强数学/数据结构,各自认领对口的题。
  • 你的记忆里提到你们是三人分工准备答辩,竞赛同理——赛前就商量好谁主攻什么方向,临场效率高。

② 电脑使用规则(关键)

  • 谁的题想清楚了、能直接开写,谁上机。 没想清楚的人绝不占着电脑空想——在草稿纸上想,把电脑让出来。
  • 上机前先在纸上把代码框架、思路理顺,坐下就开敲,减少占用电脑的时间
  • 写代码的人卡壳了(调试卡很久),及时让出电脑给有把握的队友,自己在纸上继续debug。电脑利用率最大化。

③ 信息共享

  • 谁读完一道题,简明同步给队友:「这题是个最短路,n 很小,能暴力」。
  • 卡题时换人看:当局者迷,队友的新视角常常一眼看出 bug 或想到思路。
  • 公用一张榜单/题目状态表:哪些已过、哪些在写、哪些放弃,三人随时同步,别两个人重复做同一道题。

④ 调试分离:写代码的人在电脑上,另一个人可以在纸上帮他想测试数据、查逻辑,并行工作,不浪费人力。

团队赛的核心思想:电脑是稀缺资源,要让「最能立刻产出 AC」的人使用;其余人在纸上并行工作(读题、想思路、debug、造数据)。 三个人始终都在干活,没人闲着,这才是高效团队。

七、心态与节奏(贯穿全场)

  • 开局别慌,先签到题热身,拿下第一个 AC 建立信心。
  • 中段是主战场,稳扎稳打清掉有把握的题。
  • 卡住时深呼吸,换题、换人、喝口水,别钻牛角尖。
  • 最后一小时,评估还能多拿哪道题,集中火力,同时注意封榜(最后 1 小时榜单通常冻结,看不到别人最新进度,按自己节奏打)。
  • 全程维护罚时意识,但别因为怕罚时而不敢交——该交还得交,平衡好。

八、一套完整的临场作战流程(把全节串起来)

把这个流程记下来,就是你的比赛 SOP:

  1. 开局(0~20 min):一人占机敲模板,三人分头读题贴标签,记数据范围。
  2. 同步:碰头确定开题顺序——签到题排前,难题靠后/放弃。
  3. 清签到(20 min~2 h):从最快能 AC 的题做起,稳稳拿下所有送分题,控制罚时(想清楚再交)。
  4. 攻中等题(2~4 h):挑思路最清晰的做,会暴力的暴力莽(0.8),三人按擅长方向分工,电脑给最能产出的人。
  5. 止损与冲刺(4~5 h):卡死的题果断弃,集中火力到最可能拿下的题,注意封榜后按自己节奏打。
  6. 全程:罚时意识 + 换题换人机制 + 信息同步 + 没人闲着。

第〇部分到此圆满收官! 咱们从 0.1 一路走到 0.9,把输入输出、数据溢出、易错点、评测解读、读编译错误、调试对拍、复杂度分析、偷分策略、比赛策略这套实战工程技巧全部打通了。这部分不教具体算法,但它决定了你会的算法能不能转化成实实在在的分数、能不能在赛场上稳定发挥——这正是铜牌选手和「明明会做却拿不到分」的选手之间的核心差距。

你现在手里已经有了一整套「怎么打比赛」的方法论。接下来就是往这个框架里填充真正的算法弹药了。

按我们最初定的大纲,下一步进入 第一部分:基础算法与 STL,这是所有算法的地基,也是铜牌的绝对核心。建议从 1.1 STL 精通 开始——vector / string / sort / set / map / priority_queue / lower_bound 这些是你后续每一道题都要用的工具,把它们用熟,写题速度会质的飞跃。

教练衔接coach

要不要现在开启第一部分?或者你想先就第〇部分的某些点(比如亲手搭一次对拍、或者做几道综合运用 I/O + 复杂度判断的题)巩固一下再往下走?