实战工程技巧
正课算法之前的「实战工程技巧」——输入输出、数据溢出、看代码揪错、评测结果解读、编译报错、对拍调试、复杂度估算、偷分与比赛策略。
这些不是算法本身,却是把「会做」变成「拿到分」的关键:少挂罚时、少踩坑、把会做的题又快又稳地切掉。铜牌的胜负,很多就藏在这里。
本课导读 · 知识体系大纲与两个月计划
先给你吃颗定心丸:两个月从你现在的基础(C/C++ + 贪心/二分/前缀和/DP/高精度等模板都摸过)冲区域赛铜牌水平,是现实可达的。铜牌的本质不是会多少冷门算法,而是把那 3~5 道偏简单的题又快又稳地切掉、不挂罚时。所以我们的策略很明确:少而精的知识点 + 大量做题 + 反复打模拟赛。
下面是我提议的完整大纲框架和两个月计划。你先看,咱们把它定下来,之后我再一个分支一个分支地展开细讲。
一、知识体系大纲(按铜牌优先级标注)
标注说明:【核心·必修】 铜牌必须吃透 | 【强化】 稳定输出靠它 | 【拓展】 银牌往上,两月内先放着
第〇部分:竞赛实战工程技巧 ← 你这次主要问的就是这块
这部分不教算法,但决定你「会做的题能不能拿到分」,价值极高,会单独花一整块讲。
- 0.1 输入输出技巧(
scanf/printfvscin/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,免费且权威)。
关于目标的实话:这个计划能把你的个人水平练到「铜牌该会的题能稳定切」。最终能不能拿牌还受队友、赛题难度、临场发挥影响,但技能层面,铜牌是合理且大概率够得着的目标。
这就是我提议的整体框架。在我们开始逐个展开之前,请你确认或调整:
- 这个大纲和优先级你认可吗? 有没有想加 / 想砍的部分?
- 你想从哪里开始? 我的强烈建议是先啃第〇部分(实战工程技巧)——也就是你这次问的输入输出、查错、复杂度、偷分那些,因为它立刻能让你「少挂分」,性价比最高。
你点个头或说说想从哪开始,我就把那个分支详细铺开给你。
0.1 输入输出技巧
好,第〇部分最实用的一块,咱们一次讲透。核心思路是:给你一套固定的默认写法,让你 95% 的题目根本不用思考 I/O,把脑子留给算法。
1. 先背下这个万能开头
每道题都这样开头,闭着眼睛敲:
#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":
int a, b;
while (cin >> a >> b) { // 读不到了自动退出
cout << a + b << "\n";
}cin >> 读失败(到 EOF)会返回 false,循环自然结束。对应的 scanf 写法是 while (scanf("%d %d", &a, &b) != EOF)。
(b) 第一行给组数 T——最常见:
int T;
cin >> T;
while (T--) {
// 每组的处理逻辑
}(c) 先读 n,再读 n 个数:
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 >> n 再 getline 时:
int n;
cin >> n; // 读了数字,但行末的换行符 '\n' 还留在缓冲区里
string line;
getline(cin, line); // 它读到的是那个残留的换行 → line 是空的!cin >> n 只取走数字,把后面的 \n 留在了缓冲区,结果第一次 getline 直接撞上这个 \n,读到一个空行。
最好记的解决办法,记这一个就够:
int n;
cin >> n;
string line;
getline(cin >> ws, line); // >> ws 先吃掉所有残留空白,再读整行cin >> ws 会跳过一切前导空白(空格、换行、Tab),干净利落。把它当成「getline 前的护身符」。
常用字符串场景
读一个词、读整行、读到 EOF 的标准写法:
string s;
cin >> s; // 读一个单词
string line;
getline(cin, line); // 读一整行
while (getline(cin, line)) { // 一行一行读到文件尾
// 处理 line
}小结一下你的默认套路:能用
cin >> s就用它;只有题目明确「这一行可能含空格、要整行读」时才上getline,而且getline前面加>> ws。 这样基本不会出错。
5. 输出格式与精度
整数、字符串直接 cout << x << "\n" 就行,重点是浮点数。
最简单的方式——cout 设定一次,后面一直生效:
cout << fixed << setprecision(2) << x << "\n"; // 保留 2 位小数,如 3.14fixed 表示用定点小数(不要科学计数法),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是不是写成了%lld、double读入是不是%lf。 - 换行是不是用了
"\n"而不是endl。 - 开了 sync 之后是不是手贱混用了 scanf/printf。
getline前面是不是漏了>> ws。- 浮点比较是不是用了
==(该用 eps)。 - 数字之间、行之间的分隔(一般单空格 + 换行,绝大多数判题机会自动忽略行末多余空格)。
这块就是这些,把第 1 节的模板和第 4 节的字符串规则练成肌肉记忆,I/O 基本就不会再坑你了。
要不要我接着讲 0.2 数据类型与溢出(什么时候必须开 long long、取模运算怎么不出错)?还是你想先就上面某个点做几道题练手、我给你挑题?
我记得string里面还有很多那种很零碎的东西,感觉很烦恼,像什么字符串的长度,这些函数要记吗?像同一个字符输多少个,等等
问得特别好,这正是新手对 string 最大的误区——以为要把几十个成员函数背下来。其实完全不用。竞赛里 string 真正高频的就十来个操作,剩下的用到了现查 OI-Wiki 就行。
我的原则:把 string 当成「能用下标访问、还能自动变长的字符数组」,绝大多数事情靠下标 s[i] 和循环就解决了,根本不需要记花哨函数。
一、必须记住的(就这些,背下来)
1. 长度:s.size() 或 s.length()
俩完全一样,记一个就行。我建议统一用 s.size(),因为 vector、set、map 全都是 .size(),统一心智。
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 开始),可读可写:
s[0] = 'H'; // 直接改
char c = s[2]; // 直接取3. 末尾加字符 / 拼接:+=
string s = "ab";
s += 'c'; // "abc" ——加单个字符
s += "de"; // "abcde"——加一段字符串往末尾加字符还有个 s.push_back('c'),和 += 单字符等价,记一个即可。
4. 判空:s.empty()
if (s.empty()) ... // 等价于 s.size() == 0,但更清晰5. 比较:直接用 == < >
这是 string 的福利,不像数字浮点那样有坑,直接比就行:
if (s == "yes") ... // 内容相等
if (s1 < s2) ... // 字典序比较,sort 字符串数组靠的就是它6. 排序:sort(s.begin(), s.end())
把字符串内部的字符从小到大排,竞赛里出奇地常用(判断是否同字母异序词等):
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 有个超好用的构造:
string stars(5, '*'); // 直接得到 "*****"
cout << stars;
// 或者一行
cout << string(5, '*'); // 输出 *****string(个数, 字符) 就是「把某字符重复 n 遍」。画三角形、铺空格对齐这种题,这个写法又短又不出错,强烈建议记住。
// 经典:输出一个高度为 n 的左对齐星号三角
for (int i = 1; i <= n; i++)
cout << string(i, '*') << "\n";四、配套要记的:字符本身的小技巧
字符串题经常要对单个字符做判断,这几个比背函数更值得记,因为是通用套路:
// 判断类型(需要 <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' 这俩字符技巧,就能应付铜牌阶段几乎所有字符串题。其余的,忘了是正常的,查就对了——这不丢人,是正确的做题方式。
要不要我给你出 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),不告诉你有几组。
输入样例
1 2
10 20
100 200
输出样例
3
30
300做完追加要求:再写一个「第一行给组数 T」的版本(输入第一行是 3,后面跟 3 行)。两种你都要会。
AC第 1 题 参考▸
#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 组版本:
int T;
cin >> T;
while (T--) {
int a, b;
cin >> a >> b;
cout << a + b << "\n";
}第 2 题:平均分(练浮点精度 fixed + setprecision)
题意:第一行一个整数 n,第二行 n 个整数,求平均值,保留 2 位小数输出。
输入样例
4
1 2 3 4
输出样例
2.50坑点自查:求和会不会溢出?平均值是整数除法还是浮点除法?(故意挖的,想想 10/4 和 10.0/4)
AC第 2 题 参考▸
#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 行,每行是一句话(含空格),原样输出每一行,并在前面加上行号。
输入样例
3
hello world
I love ACM
good luck
输出样例
1: hello world
2: I love ACM
3: good luck这题就是专门练那个经典 bug:cin >> n 之后直接 getline 会读到空行。看你怎么处理。
AC第 3 题 参考▸
#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 行顶格)。
输入样例
4
输出样例
*
**
***
****提示:第 i 行(i 从 1 到 n)= (n-i) 个空格 + i 个星号。用 string 重复构造,别手写循环拼。
下面是提示和参考答案,自己写完再看:
AC第 4 题 参考▸
#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题字符重复输出。
你可以把代码写好贴给我,我帮你看哪里有隐患(哪怕能过样例,我也会指出潜在的 WA 点)。或者全部搞定后,我们就推进到 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¹⁹),铜牌阶段几乎用不到,知道有就行。
二、头号杀手:乘法溢出(最重要,重点看)
看这段代码,几乎每个新手都栽过:
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 计算:
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。 这是新手最划算的保险。
三、一个偷懒大招(你会在别人代码里见到)
很多竞赛选手图省事,直接在开头写:
#define int long long
signed main() { // 注意:main 必须改成 signed,不能再是 int
// ...
return 0;
}这样全文的 int 都变成 long long,一劳永逸躲掉所有乘法溢出。代价是:内存翻倍(开大数组时可能 MLE)、速度略慢。
我的建议:铜牌阶段题目数据不大,这招很香,可以用。但你得知道两点——① main 要改成 signed main();② 万一开了上千万的大数组导致 MLE,就老老实实手动管理类型。先用着,遇到内存问题再说。
四、取模运算(竞赛超高频,单独讲)
很多题答案极大(比如方案数),题目会要求输出「答案对 10⁹+7 取模的结果」。两个经典模数你会反复见到:
const int MOD = 1e9 + 7; // 即 1000000007,最常见
const int MOD = 998244353; // 第二常见取模的运算规则,加、减、乘可以随意拆,除法不行(除法要用逆元,那是 5.3 的内容,现在先不管):
// 加法
(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 了。
万能修正写法,记住这个固定模板:
// 减法取模,防止出现负数
long long res = ((a - b) % MOD + MOD) % MOD;
// ↑ 先 +MOD 拉回正数,再 %MOD+ MOD 再 % MOD 能保证结果一定落在 [0, MOD)。这就是取模铁律:
减法取模后,永远写成
((a - b) % MOD + MOD) % MOD,自动消灭负数。
累乘要边乘边取模
算 n! 这种连乘,不能等乘完再取模(早就溢出了),要每乘一次就取一次模:
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 基本绝迹。
要不要我出 2~3 道专扎溢出和取模的小题(比如「求 1 到 n 的和」「n 的阶乘取模」这种,看着简单但不开 long long 必挂)练一下手感?还是直接推进 0.3 常见易错点清单?
配套练习题
好,给你 4 道题,每一道都长着「人畜无害」的脸,但不开 long long 或不处理取模就必 WA。这正是溢出的阴险之处——样例小的时候全过,一交就挂。
先自己写,写完再看折叠的答案和陷阱分析。
第 1 题:区间求和(练「求和溢出」+ 估算最大值)
题意:给你一个整数 n(1 ≤ n ≤ 10⁹),求 1 + 2 + 3 + … + n 的结果。
输入样例
100000
输出样例
5000050000自查:结果最大能到多少?用 int 存得下吗?(先估,再写)
AC第 1 题 参考▸
#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×…×n 对 1000000007 取余。
输入样例
10
输出样例
3628800输入样例
20
输出样例
146326063自查:fact * i 这一步,两个数都接近 10⁹ 时乘积多大?该用什么类型存?
AC第 2 题 参考▸
#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。
输入样例
100000 100000
1000000000 1000000000
3 4
输出样例
10000000000
1000000000000000000
12自查:a、b 用什么类型读?a * b 怎么写才不会在乘的过程中溢出?(这题就是 0.2 第二节那个核心坑的裸题)
AC第 3 题 参考▸
#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;
}这里把 a、b 直接读成 long long,最省事。
如果你坚持用 int 读,那就必须强转:
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 修正)
题意:给你两个整数 a 和 b(0 ≤ a, b ≤ 10⁹),输出 (a − b) mod (10⁹+7),要求结果是 [0, 10⁹+7) 范围内的非负整数。
输入样例
10 3
输出样例
7输入样例
3 10
输出样例
999999990自查:第二组 3 - 10 = -7,C++ 里 (-7) % MOD 会是负数,直接输出就 WA。怎么修正成 999999990?(提示:1000000007 - 7 = 1000000000,对上了)
下面是参考答案,自己写完再对:
AC第 4 题 参考▸
#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题负数取模修正。
把代码写出来贴给我,我帮你逐个挑隐患(尤其看你 long long 开得对不对、取模套路对不对)。或者你都过了,我们就推进 0.3 常见易错点清单。
0.3 常见易错点清单
这一节是「血泪经验合集」——这些坑你几乎一定会踩,区别只在于踩几次。提前看一遍,能帮你在赛场上把「调一小时」变成「扫一眼就发现」。
我按踩坑频率从高到低排,重点看前三个。
一、多组数据不清空(频率★★★★★,最阴险)
这是多测题的头号杀手。你写对了单组逻辑,但忘了上一组的数据残留,第二组就用着脏数据,导致第一组样例过、后面全错或者偶尔过偶尔挂。
看这个典型错误:
int T;
cin >> T;
while (T--) {
int sum = 0;
int cnt; // ❌ 没初始化,但这里其实还好(下面会赋值)
static int a[100005]; // ❌ 用了上一组留下的旧数据!
// ... 处理
}问题场景:全局数组、计数器、标记数组、答案变量,如果在循环外定义或忘了重置,第二组就带着第一组的「记忆」。
对策——养成习惯:每组数据开头,把所有用到的东西清零。
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。这时只清空 0 到 n 即可: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=1、n=0、全相同、最大值。这比交上去 WA 再猜强一百倍。0.6 节会专门讲怎么系统地造数据。
举个边界经典翻车:
// 求 n 个数的最大值
int ans = 0; // ❌ 如果所有数都是负数,答案会错成 0
for (int i = 0; i < n; i++) ans = max(ans, a[i]);// ✅ 正确:初值设成第一个元素,或设成极小值
int ans = a[0]; // 或 int ans = -2e9;
for (int i = 1; i < n; i++) ans = max(ans, a[i]);初值设 0 是个高频错误——求最大值的初值应该是「极小」,求最小值的初值应该是「极大」,别习惯性写 0。
四、忘记初始化(频率★★★★☆)
C++ 里局部变量不会自动清零,里面是随机的垃圾值。忘了赋初值,行为完全不可预测(本地能过、评测机挂,因为内存状态不同)。
int sum; // ❌ sum 是垃圾值
for (int i = 0; i < n; i++) sum += a[i]; // 在垃圾值基础上累加int sum = 0; // ✅ 累加器、计数器一定要初始化注意区别:
- 局部变量(函数内
int x;):垃圾值,必须手动初始化。 - 全局变量 / 全局数组(函数外定义):自动初始化为 0,可以不手动清——但多测时仍要每组重新清空(见第一条)。
习惯:累加器、计数器、答案、标记,定义时顺手就赋初值,
int sum = 0, cnt = 0, ans = 0;。
五、浮点比较(频率★★★☆☆)
0.1 提过,这里归档进清单。浮点数因为精度误差,两个「相等」的数可能差一丝丝:
if (a == b) // ❌ 浮点直接比,可能因精度误差判错
if (fabs(a - b) < 1e-9) // ✅ 看差的绝对值是否足够小延伸的判断:
// 判断 a 是否 ≥ b(浮点)
if (a > b - 1e-9) // 加个 eps 容差
// 判断 a 是否为 0
if (fabs(a) < 1e-9)能用整数就别用浮点。很多看起来要小数的题,其实可以全程用整数算(比如比较
a/b和c/d,交叉相乘变成a*d和c*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。 int和size_t混用:s.size() - 1当 s 为空时变巨大正数(0.2 强调过)。
七、提交前 30 秒自查清单
按这个顺序在脑子里过一遍,能拦住绝大多数低级 WA:
- 多测清空了吗?(数组、标记、计数器、答案)
- 数组开够大了吗?(
+5缓冲,下标从 1 要n+1) - 边界试了吗?(n=0、n=1、全相同、最大值)
- 累加器/答案初始化了吗?(求 max 别初始化成 0)
- 该开 long long 吗?(接上节 0.2)
- 浮点用 eps 比了吗?
这六条 + 那张自查清单,是你未来每场比赛的「保命符」。我自己写题,交之前都会扫一遍这个清单,它能拦掉的罚时,比多会一个算法值钱得多。
要不要我故意写几段藏着这些 bug 的代码,让你来找茬(专门练「看代码揪错」的能力,这正是赛场上 debug 的核心技能)?还是直接推进 0.4 评测结果解读与对策(AC/WA/TLE/MLE/RE/CE 各是什么、遇到怎么办)?
找茬练习
好,这是个极其重要的能力——赛场上 80% 的时间在 debug,能一眼揪出 bug 的人罚时少一大截。
给你 6 段代码,每段都有 1~2 个 0.3 清单里的经典 bug。你的任务:找出每段的 bug 在第几行、是什么问题、怎么改。
建议你先把每段都看完、写下你的判断,再展开对照答案。看代码找 bug 比自己写还难,慢慢来。
第 1 段:求 n 个数里的最大值
#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)。
改法:初值设成第一个元素,或设成极小值:
int maxv = a[0]; // 推荐
// 或 int maxv = -2e9;这就是清单第三条「求 max 别初始化成 0」的活例子。
(附带一提:int a[n]; 这种变长数组在竞赛里能用但不规范,n 大时会栈溢出,建议开全局大数组 int a[N];。)
第 2 段:多组数据,每组统计有几个正数
#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;
}输入:
2
3
1 -2 3
2
-1 5期望输出 2 然后 1。这段会输出什么?
AC第 2 段 答案▸
Bug:cnt 是全局变量(第 3 行),多组之间没有清空。
第一组数完 cnt = 2,进入第二组时 cnt 还是 2,第二组又数到 1 个正数,变成 3,输出 2 然后 3(错,第二组应为 1)。
这是清单第一条「多测不清空」的头号杀手,最阴险,因为第一组永远是对的。
改法:把 cnt 移到循环内部(自动重置),或每组开头清零:
while (T--) {
int cnt = 0; // 移进来,每组自动归零
// ...
}第 3 段:计算 1×2×3×…×n
#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,评测机上大概率是乱码。这是清单第四条「忘记初始化」。
改法:
long long product = 1; // 累乘的初值是 1(累加才是 0)注意:累乘初值是 1,累加初值是 0,别搞混。
第 4 段:读 n 个数存进数组,再倒序输出
#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。
改法:数组多开几个:
const int N = 100005; // 多开 5 个缓冲,下标从 1 用就安全了养成 1e5 + 5 的肌肉记忆,专治这种 off-by-one。
第 5 段:判断两个分数 a/b 和 c/d 是否相等
#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/3 和 2.0/6 在数学上相等,但浮点运算有精度误差,二进制存储下可能差一个极小值,== 判断为不等,输出 "not equal"(错)。这是清单第五条。
改法:用 eps 容差:
if (fabs(a / b - c / d) < 1e-9)更好的改法(避坑思维):分数比较交叉相乘,全程用整数,没有任何精度问题:
// 若 a b c d 是整数,判断 a/b == c/d 等价于 a*d == c*b
if ((long long)a * d == (long long)c * b) // 注意防溢出能转整数就转整数,这是高手的习惯。
第 6 段:判断一个数是奇数还是偶数
#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。这是清单第六条。
改法:给位运算加括号:
if ((x & 1) == 0) // 先算 x&1,再比较凡是 & | ^ 和比较运算混在一起,一律加括号,别赌优先级。
找对了几个? 这 6 个 bug 对应清单的 6 大类:
- 求 max 初值为 0
- 多测不清空
- 局部变量没初始化(且累乘初值应为 1)
- 数组越界(下标从 1 没留空间)
- 浮点
==比较 - 位运算优先级
如果你能一眼揪出 4 个以上,赛场 debug 能力已经不错了。没找全很正常——第 2、3、4 段最难,因为它们「能过样例」,这正是最坑的那类。
可以告诉我你找对了几个、哪几段卡住了,我针对性再补几道;或者推进 0.4 评测结果解读与对策(AC/WA/TLE/MLE/RE/CE 遇到各自怎么办)。
0.4 评测结果解读与对策
这一节是赛场救命指南。提交后那个红色或绿色的结果,不是终点,而是给你的线索。会读这些缩写、知道每种该往哪个方向查,能让你从「无头苍蝇乱改」变成「精准定位」。
先给你一张总览表,然后逐个展开对策。
| 缩写 | 全称 | 中文 | 一句话含义 |
|---|---|---|---|
| AC | Accepted | 通过 | 恭喜,过了 ✅ |
| WA | Wrong Answer | 答案错误 | 跑完了,但答案不对 |
| TLE | Time Limit Exceeded | 超时 | 跑太慢,超过时限 |
| MLE | Memory Limit Exceeded | 超内存 | 用的内存太多 |
| RE | Runtime Error | 运行时错误 | 程序中途崩溃 |
| CE | Compile Error | 编译错误 | 没编译成功,根本没跑 |
| PE | Presentation Error | 格式错误 | 答案对,但格式(空格/换行)不对 |
| OLE | Output Limit Exceeded | 输出超限 | 输出太多(通常是死循环狂打印) |
下面按你最常遇到的顺序逐个讲怎么办。
一、WA(答案错误)——最常见,也最需要冷静
含义:程序正常跑完了,但输出和标准答案不一致。注意:通常判题机只告诉你 WA,不告诉你错在哪个测试点(有些平台会显示「第 3 个点 WA」)。
排查顺序(按这个来,别瞎改):
- 重新读题。WA 第一反应永远是「我题意理解错了吗?」——看错数据范围、漏看某个条件、输出要求理解偏了,占 WA 的一大半。把题目再读一遍,比改代码有用。
- 过自查清单(就是 0.3 那张):多测没清空?数组越界?边界没处理?初值错了?该开 long long 没开?浮点用 == 了?——挨个排除。
- 手造边界数据自测:
n=1、n=0、全相同、全负数、最大值。你的代码大概率死在某个边界。 - 如果显示了「第几个点 WA」:第 1、2 个点就 WA,通常是基础逻辑或读题错;前面都过、最后几个点 WA,通常是边界 / 溢出 / 超大数据下的特殊情况。
- 还找不到?上对拍(0.6 会专门讲):写个暴力解 + 随机数据生成器,让正确的暴力和你的代码对跑,揪出第一组答案不同的数据。这是终极武器。
WA 心态:别一看到 WA 就慌着乱改代码。先想,再改。 盲目改往往越改越错,还浪费罚时。
二、TLE(超时)——你的算法可能不对
含义:程序在规定时间内(通常 1~2 秒)没跑完。
关键认知:TLE 几乎总是意味着算法复杂度不对,而不是「写得不够优化」。靠抠常数(卡常)救回来的是少数。
排查顺序:
- 先看复杂度(接 0.7 的内容):拿数据范围
n反推。n=10⁵你写了 O(n²)(10¹⁰ 次运算)必超。正确做法是换更优的算法,比如 O(n²)→O(n log n)。这是治本。 - 检查有没有死循环:
while条件永远不满足、for里下标没正常递增。死循环也会 TLE(或 OLE)。 - 检查 I/O 慢:用了
cin/cout但忘了加sync_with_stdio(false),或者用了endl狂刷缓冲。大数据量下这能让你从 AC 变 TLE。回去看 0.1 的万能开头加了没。 - 常数优化(最后手段,治标):减少不必要的重复计算、
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 原因(按频率):
- 数组越界(占绝大多数):访问了
a[-1]、a[n](开小了)、下标超范围。回去看 0.3 第二条。数组开大一点经常直接解决。 - 除以 0:
a / b或a % b时b == 0。特别是计算里b可能为 0 的情况要特判。 - 栈溢出:递归太深(DFS 没设终止条件,或递归层数过大),或者开了巨大的局部数组(
int a[10000000];写在 main 里,应该放全局)。 - 空指针 / 空容器访问:
vector是空的却访问v[0]、v.back();空string访问s[0]。 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 个 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)
- 二维数组尤其当心:
a[5000][5000]= 2.5×10⁷ × 4 = 100MB,接近上限。开二维前先乘一下。 - 别开不必要的大数组,或者用
vector按需分配。 #define int long long会让内存翻倍(0.2 提过),大数组场景下可能正是 MLE 元凶。
六、PE(格式错误)——答案对,格式错
含义:答案内容是对的,但输出格式不符——多了/少了空格、换行,或大小写不对。不是所有平台都有 PE,很多平台对格式很宽容,把这种直接算 AC 或 WA。
对策:
- 检查行末多余空格、最后是否该有换行。
- 检查输出的单词大小写(
YesvsYESvsyes,题目要哪个就用哪个)。 - 数字之间的分隔符(空格 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 编译错误怎么读
这一节专治「一堆英文报错糊脸上,瞬间懵掉」。好消息是:编译器其实很啰嗦地在告诉你哪行错、错什么,你只要会读前几行就行。学会之后,CE 对你来说会变成「扫一眼、改一下、5 秒解决」的小事。
一、读报错的三条总原则(最重要)
在认具体报错之前,先建立正确的读法,这比记单词更关键:
① 永远从第一条 error 开始看,别被吓到。 编译器一个错误常常引发后面一连串「连锁报错」。第一条 error 是病根,后面的多半是它的连锁反应。所以——看到几十行红字,只读最上面那条 error,改完重新编译,往往后面全消失了。
② 分清 error 和 warning。
error(错误):必须改,否则编译不过、程序跑不了。warning(警告):能跑,但提醒你可能有问题。比如「变量未使用」「比较有符号和无符号数」。竞赛里 warning 一般可以忽略(但有些 warning 其实在暗示 bug,比如=写成==,值得瞄一眼)。
只有 error 会导致 CE。 满屏 warning 但没有 error,是能正常提交的。
③ 锁定行号。 报错格式基本是这样:
main.cpp:8:15: error: 'x' was not declared in this scope
↑ ↑ ↑
文件名 行号:列号 错误类型和说明8:15 就是第 8 行第 15 列。直接跳到第 8 行去看。列号有时不准,但行号很可靠(除了漏分号那种,见下文)。
二、最高频报错中英对照(重点背这几个)
下面是你 99% 会遇到的报错,配上中文意思和怎么改。
1. 变量未声明 / 拼写错误(最常见)
error: 'x' was not declared in this scope中文:x 在当前作用域没有声明。 原因:变量名拼错了、忘了定义、或者用了作用域外的变量(比如在 if 里定义的变量,到外面用)。 改法:检查 x 是不是拼错(cnt 写成 cont)、是不是忘了 int x;、是不是定义在了别的花括号里。
2. 缺少分号(最坑,因为报错指向下一行)
error: expected ';' before '...'中文:在某处之前期望一个分号 ;。 原因:上一句末尾漏了分号。 关键技巧:报错指向的行往往是对的,但分号要加在它的上一行末尾! 比如报错说第 9 行有问题,实际是第 8 行末尾漏了 ;。看到 expected ';',先看上一行。
3. 括号不匹配
error: expected '}' at end of input
error: expected ')' before '...'中文:在输入末尾期望一个 } / 在某处之前期望 )。 原因:少了右花括号 } 或右括号 ),或者多了。 改法:检查括号配对。养成写代码时左右括号一起打、再往里填内容的习惯,能避免大部分这类错误。编辑器的括号高亮也能帮你找。
4. 函数/类型不存在(常常是没 include 或拼错)
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. 类型不匹配
error: cannot convert 'std::string' to 'int' in initialization
error: invalid conversion from 'int' to 'char*'中文:无法把 string 转成 int / 无法把 int 转成 char*。 原因:把不兼容的类型互相赋值,比如想把字符串直接当数字用。 改法:检查变量类型,该转换的用 stoi、to_string 等正确转换。
6. 重复定义
error: redefinition of 'x'
error: redeclaration of 'int x'中文:x 重复定义了。 原因:同一个作用域里定义了两次同名变量,比如 int x; 写了两遍。 改法:删掉重复的定义,或改名。
7. 赋值给常量 / 左值错误
error: lvalue required as left operand of assignment中文:赋值号左边需要一个可赋值的对象(左值)。 原因:通常是 == 写成了 = 的反面——把表达式放在了赋值号左边,或者给常量赋值。 改法:检查赋值语句,左边必须是变量。
8. main 函数相关
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:
warning: suggest parentheses around assignment used as truth value含义:你在 if 里用了 =(赋值)而不是 ==(比较)。比如 if (x = 5)。这几乎一定是 bug!(0.3 清单提过)
warning: comparison between signed and unsigned integer expressions含义:有符号和无符号数比较(常见于 int i 和 s.size() 比较)。多数时候没事,但极端情况会出 bug(0.2 那个 s.size()-1 空串变巨大正数的坑)。
四、实战:拿到一屏报错,怎么操作
给你一套固定流程,照做就行:
- 滚到最上面,找第一条
error(忽略 warning,忽略后面的连锁 error)。 - 看行号,跳到那一行。
- 读错误类型,对照上面的表(或看关键词:
not declared=没定义/拼错,expected ';'=上一行漏分号,expected '}'=括号不配对)。 - 改掉,重新编译。
- 如果还有 error,重复 1~4。一次只改一个,别一口气猜着改一堆。
黄金心法:改一个,编译一次。 新手最容易犯的错是看到 5 个 error 就同时猜着改 5 处,结果改出新错误,更乱。逐个击破最快。
五、本地编译命令(强烈建议配一个)
如果你在本地写代码,用这条命令编译能得到清晰的报错,还能开额外检查:
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 调试与快速查错
这是第〇部分实战技巧的压轴重点。前面教你「怎么读评测结果的线索」,这一节教你主动出击、自己把 bug 揪出来——尤其是「对拍」这个武器,能让你在没有标准答案的情况下,自己造出让代码出错的数据。掌握它,你查 WA 的能力会上一个台阶。
我按从易到难、从日常到终极的顺序讲。
一、防御性写法(治未病,写的时候就少埋雷)
最好的调试是不产生 bug。这些习惯能从源头堵住 0.3 清单里的大部分坑:
① 数组习惯性多开、用大常数。
const int N = 1e5 + 5; // 固定写法,多开 5 个缓冲
int a[N], vis[N]; // 全局定义,自动初始化为 0下标从 1 用就不会越界,全局数组还自动清零,一举两得。
② 累加器/答案定义即初始化。
int sum = 0, cnt = 0;
int ans = 0; // 求和类
int maxv = -2e9, minv = 2e9; // 求极值类,初值给极端③ 多测在循环开头统一清空。
while (T--) {
memset(vis, 0, sizeof(vis)); // 或只清前 n 个
int ans = 0; // 局部变量自动重置
// ...
}④ 复杂表达式拆开 + 加括号。 别把一长串运算挤一行,拆成几步更好查,位运算/比较一律加括号(防 0.3 那个优先级坑)。
⑤ 写完先估复杂度、估内存、估溢出。 提交前花 10 秒:数据范围反推复杂度会不会 TLE?数组多大会不会 MLE?有没有乘法要开 long long?这三件事想清楚,省下大把罚时。
防御性写法的核心思想:把容易错的地方变成固定的肌肉记忆,这样你写代码时根本不用思考就是对的,脑子专注于算法。
二、构造边界数据自测(提交前的必修课)
别一写完就提交。先自己造几组极端数据喂给程序,很多 bug 当场现形。必造的几类:
- 最小:
n = 0、n = 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 所在。
对拍需要四个文件
std.cpp——暴力解(慢但保证对)。my.cpp——你的正解(快但怀疑有 bug)。gen.cpp(或脚本)——随机数据生成器。- 对拍脚本——自动反复生成数据、跑两个程序、比对结果。
第一步:写暴力解 std.cpp
用最朴素、最不会错的方法解同一题。比如「求区间和」,正解用前缀和 O(1),暴力就老老实实 for 循环累加 O(n);「求最优方案」,正解用 DP,暴力就全排列枚举所有可能。慢没关系,对就行。
第二步:写数据生成器 gen.cpp
生成符合题目格式的随机小数据(注意:对拍用的数据要小,比如 n 只取 1~10,这样暴力跑得动,反例也好读)。
#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:
@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
pausefc 是 Windows 的文件比较命令,%random% 是系统随机数(当种子)。一旦两个输出不同(fc 报错),停下来,in.txt 里就是反例。
Linux/Mac 用 .sh,新建 duipai.sh:
#!/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"
donediff 是 Linux 的文件比较命令。运行前先 chmod +x duipai.sh,再 ./duipai.sh。
第四步:编译运行
# 先把三个程序都编译好
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 就在那附近。
cout << "DEBUG: i=" << i << " sum=" << sum << "\n"; // 临时调试输出提示:调试用的
cout记得提交前删掉或注释,否则会污染输出导致 WA / PE。有个常用技巧是用宏控制:#define debug(x) cerr << #x << " = " << x << "\n"用
cerr(标准错误流)输出调试信息,它不会进入提交的答案(判题机只看标准输出 stdout),本地却能看到。debug(sum)会打印sum = 42。这个小技巧很实用。
五、善用 sanitizer 抓越界/内存错误(查 RE 神器)
接 0.5 提过的,本地编译加上这个参数:
g++ -fsanitize=address -g my.cpp -o my然后正常运行,一旦发生数组越界、访问非法内存,它会直接打印出错在哪一行、什么类型的错误。查 RE 和隐蔽越界(那种不崩但 WA 的)特别好用。本地跑一遍带 sanitizer 的版本,很多隐患当场暴露。
六、查错流程总结(把前面串起来)
遇到 WA/RE 找不到原因,按这个漏斗式顺序走:
- 重读题 → 确认没理解错(最高频,先做这个)。
- 过 0.3 自查清单 → 越界?没清空?初值?溢出?
- 造边界数据自测 → n=0/1、全相同、最大值。
- 插
cerr打印中间变量 / 二分注释定位 → 缩小到具体哪几行。 - 带
-fsanitize=address跑 → 抓越界/内存错误(针对 RE)。 - 上对拍 → 前面都没招时的终极武器,自动揪反例。
越往后越重,但越往后越能解决「肉眼找不到」的硬骨头。日常小 bug 走到第 3 步基本就解决了,对拍是留给真正难缠的 WA 的。
这一节的灵魂:调试不是「盯着代码空想」,而是有方法、有工具、有流程地主动逼出 bug。新手 debug 靠运气和直觉,高手 debug 靠对拍、sanitizer 和系统化的排查顺序。你把「对拍」亲手搭一次、把 cerr 调试和 sanitizer 用熟,赛场上的查错效率会和过去判若两人。
到这里,第〇部分还剩两块:
- 0.7 时间空间复杂度分析——其实前面 TLE 那节已经剧透了核心(数据范围反推复杂度那张表),这节会系统讲怎么计算和估算复杂度。
- 0.8 偷分 / 拿部分分策略——做不出整题时怎么捞分(暴力、打表、特判、骗分)。
还有你最初提到的 0.9 比赛策略(读题顺序、开题取舍、罚时管理)。
建议接着讲 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),复杂度要再乘一层。
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~11 | O(n!) | 全排列暴力枚举 |
| n ≤ 20~25 | O(2ⁿ) | 状压枚举、子集枚举、爆搜 |
| n ≤ 100 | O(n³) | Floyd、区间 DP、三重循环 |
| n ≤ 500 | O(n³) | 同上,n³≈1.25×10⁸ 临界但常能过 |
| n ≤ 5000 | O(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) 但常数大、最坏会退化,铜牌阶段够用但心里有数。vector的push_back均摊 O(1),但在循环里反复v.insert(开头)是 O(n),容易写出隐藏的 O(n²)。
③ 多组数据要乘组数。 如果有 T 组数据,总复杂度是 T × 单组复杂度。单组 O(n) 看着没事,但 T=10⁴、n=10⁴ 时总量 10⁸,到临界了。多测时一定把组数算进去。
七、一个完整的实战推演
给你演示拿到题怎么用复杂度思考。假设题目:「给 n 个数(n ≤ 10⁵),问有多少对数之和等于 k」。
思考过程:
- 看数据范围
n ≤ 10⁵→ 目标 O(n log n) 或 O(n),O(n²) 的暴力(枚举所有对,10¹⁰)排除。 - O(n²) 是什么?两层循环枚举每一对 → 太慢。
- 怎么降到 O(n log n)?排序 + 双指针,或者用哈希表 O(n):遍历每个数 x,查
k-x出现过几次。 - 选哈希表方案,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 比赛策略——读题顺序、开题取舍、卡题止损、罚时管理、队内配合。
这两块偏「战术」,篇幅比前面短。要不要一起讲完,给第〇部分收个尾?
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 赛制神器): 有些题答案需要超长时间计算,但输入范围有限。你可以在本地慢慢把所有可能的答案算出来,存成一个数组,直接写进提交代码。
// 比如答案只和 n(1~50)有关,本地算好存数组
int ans[] = {0, 1, 1, 2, 3, 5, 8, ...}; // 直接查表输出
cout << ans[n] << "\n";适用:打表找规律题、答案空间小但单次计算慢的题。
② 找规律打表(两种赛制都可能用): 当你看不出数学规律时,写个暴力程序把前几项答案打印出来,人工观察规律:
// 暴力跑出 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 经常就是「特殊性质」档,比如「保证图是一棵树」「保证所有数相等」,针对这些特殊性质写专门代码骗分。
if (n == 1) { cout << a[0] << "\n"; return 0; } // 特判小情况
if (所有数都相等) { /* 简单逻辑 */ } // 特判特殊结构
// 一般情况...特判的价值:很多题的一般情况很难,但某些特殊情况是送分的。先把送分的特判掉,至少不空手。在 OI 赛制这直接换分,在 ICPC 赛制这是保证你正解代码在边界不挂的必要手段。
四、贪心猜结论(高风险高回报)
核心:很多题(尤其 Codeforces 风格)的正解是一个简洁的贪心结论或数学结论,证明很难但结论很短。比赛时——大胆猜结论,用小数据/样例验证,对了就交。
实战做法:
- 观察样例,猜一个贪心策略(排序后取两端?优先选最大的?配对?)。
- 不急着证明(赛场上证明太费时),先用暴力对拍(0.6 的对拍!)验证你的猜想在小数据上对不对。
- 对拍跑几千组都一致 → 大概率是对的,直接交。
这是 Codeforces 上分的核心套路之一:猜 + 对拍验证 > 严格证明。很多 Div2 的 A、B、C 题,结论一两行,但你要是非得证出来才敢写,时间就没了。对拍就是你「不证而信」的底气——这也是为什么 0.6 要你务必学会对拍。
风险提示:猜错了会 WA + 罚时。所以一定先对拍验证再交,别凭感觉裸交。
五、骗分(兜底,纯捞分)
核心:实在不会,输出一个可能蒙对的答案,万一中了就是白赚。主要用于 OI 赛制(有部分分),ICPC 赛制意义不大(要么全对要么没分,骗中整道题的概率极低)。
OI 赛制常见骗分手法:
- 输出 0 / 输出固定值:有些计数题、判定题,答案是 0 或 "NO" 的测试点占一定比例,全输出 0 可能白嫖几个点。
- 输出样例答案:极端情况下,把样例输出直接打印,万一有个测试点就是样例……(聊胜于无)。
- 随机输出 / 输出输入本身:某些构造题,输出输入原样可能恰好合法。
- 贪心兜底:想不出正解,用一个看起来合理的贪心,能骗多少点是多少点。
骗分是最后的兜底,心态是「反正不会,扔个答案碰碰运气,不亏」。在 ICPC 赛制,与其骗分,不如把时间投到另一道有把握的简单题上。
六、ICPC 赛制下的「偷分」真谛(敲黑板)
因为你打区域赛,这段最重要。ICPC 没有部分分,所以「偷分」的真正含义是战略层面的得分最大化:
- 快速扫一遍所有题,挑最简单的先做(队伍要分工读题,0.9 会讲)。铜牌靠的是把签到题和稍简单的题又快又稳地 AC,不是死磕难题。
- 数据范围小的题,用暴力骗过整道——这是 ICPC 最实际的「偷分」。比如一道题看着要高级算法,但
n ≤ 100,你用 O(n³) 暴力直接 AC,省下想正解的时间。 - 会做的题别挂罚时:先在本地/草稿想清楚边界、造好数据,有把握再交,避免反复 WA 累积罚时。罚时管理本身就是「省分」。
- 果断弃题止损:一道题卡 40 分钟没思路,立刻换题,别让一道难题拖垮整场。会放弃,也是一种得分策略。
一句话:ICPC 的「偷分」= 选对题 + 暴力莽下数据弱的题 + 不挂罚时 + 及时弃题。 这比任何花哨技巧都重要,是铜牌的核心打法。
七、本节速记卡
- 先认赛制:ICPC(全或无,你的目标)vs OI(按点给分)。策略完全不同。
- 暴力:永远先写朴素解。数据范围小(n≤100/1000)时,暴力就是正解。
- 打表:本地算好存数组(OI);暴力打印前几项找规律(通用)。
- 特判:n=1、全相同等特殊情况单独处理,先拿送分点。
- 猜结论:大胆猜贪心/数学结论,对拍验证后再交(不证而信的底气来自对拍)。
- 骗分:OI 兜底捞分;ICPC 意义不大,不如转战其他题。
- ICPC 偷分真谛:选简单题、暴力莽弱数据题、控罚时、及时弃题。
偷分策略的本质是比赛思维的成熟:你追求的是这场比赛的总分最高,而不是某一道题的完美。一个会暴力捞分、会及时弃题、会控制罚时的选手,比一个只会死磕正解的选手,排名往往更高。 这种功利但理性的心态,正是从「会写代码」到「会打比赛」的跨越。
第〇部分就剩最后一块了——0.9 比赛策略(读题顺序、开题取舍、卡题止损、罚时管理、队内三人配合),它会把前面所有工程技巧整合成一套完整的临场作战流程。讲完这块,咱们这套「实战工程技巧」就圆满收官,可以进入第一部分的算法正题了。要收尾吗?
0.9 比赛策略
这是第〇部分的收官,也是把前面所有技巧整合成临场作战流程的一节。前面教的是「单兵作战能力」,这一节教的是「怎么打一整场比赛」——同样的实力,会打和不会打,名次能差出一大截。ICPC 是三人一队、共用一台电脑、5 小时的团队赛,策略尤其重要。
一、ICPC 比赛的基本盘(先明确规则)
- 三人一队,一台电脑,5 小时,通常 10~13 道题。
- 排名:先比 AC 题数,题数相同比总罚时(罚时越少越靠前)。
- 罚时计算:每道 AC 的题 = 通过时刻(从开赛算起的分钟数)+ 该题之前每次 WA × 20 分钟。没 AC 的题不计罚时。
- 铜牌定位:通常是中下游档位,稳定 AC 掉那几道签到题 + 1~3 道稍难的题,控制好罚时,就能摸到铜牌线。铜牌不需要你做出压轴难题。
记住这个赛制特点,下面所有策略都是围绕「多过题 + 少罚时」这个排名规则设计的。
二、开局:抢读题,铺开战场(前 20 分钟最关键)
开局绝对不要三个人挤着看第一题。最优开局是分工读题,快速摸清全场难度分布:
标准开局流程:
- 一人立刻抢电脑,敲好万能模板(0.1 那个开头),调试好环境,准备做最快能确定的签到题。
- 三人分头读题:比如按题号 A-B-C-D 分给三人,或者从两端往中间读,目标是 20 分钟内把所有题大致扫一遍。
- 边读边给题目「贴标签」:每道题在心里/草稿纸上标记——
- ✅ 签到题(一眼会,立刻能写)
- 🟡 中等题(有思路,要点时间)
- 🔴 难题/没思路(先放着)
- 还要记下数据范围(决定能不能暴力,接 0.7/0.8)。
- 快速同步:读完碰个头,「哪些是签到、哪些有思路、哪些先放弃」,确定开题顺序。
核心原则:一定要先把所有题扫完再决定做什么。 最忌讳从 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:
- 开局(0~20 min):一人占机敲模板,三人分头读题贴标签,记数据范围。
- 同步:碰头确定开题顺序——签到题排前,难题靠后/放弃。
- 清签到(20 min~2 h):从最快能 AC 的题做起,稳稳拿下所有送分题,控制罚时(想清楚再交)。
- 攻中等题(2~4 h):挑思路最清晰的做,会暴力的暴力莽(0.8),三人按擅长方向分工,电脑给最能产出的人。
- 止损与冲刺(4~5 h):卡死的题果断弃,集中火力到最可能拿下的题,注意封榜后按自己节奏打。
- 全程:罚时意识 + 换题换人机制 + 信息同步 + 没人闲着。
第〇部分到此圆满收官! 咱们从 0.1 一路走到 0.9,把输入输出、数据溢出、易错点、评测解读、读编译错误、调试对拍、复杂度分析、偷分策略、比赛策略这套实战工程技巧全部打通了。这部分不教具体算法,但它决定了你会的算法能不能转化成实实在在的分数、能不能在赛场上稳定发挥——这正是铜牌选手和「明明会做却拿不到分」的选手之间的核心差距。
你现在手里已经有了一整套「怎么打比赛」的方法论。接下来就是往这个框架里填充真正的算法弹药了。
按我们最初定的大纲,下一步进入 第一部分:基础算法与 STL,这是所有算法的地基,也是铜牌的绝对核心。建议从 1.1 STL 精通 开始——vector / string / sort / set / map / priority_queue / lower_bound 这些是你后续每一道题都要用的工具,把它们用熟,写题速度会质的飞跃。
要不要现在开启第一部分?或者你想先就第〇部分的某些点(比如亲手搭一次对拍、或者做几道综合运用 I/O + 复杂度判断的题)巩固一下再往下走?