简介:面对 OI、ACM、PAT、CSP 等算法竞赛的高强度限时环境,一套经过验证的代码模板能显著提升编码效率;这套资源正是为参赛选手与刷题者准备的常用模板合集,覆盖数据结构、排序搜索、动态规划、贪心回溯、数学数论、字符串匹配、图论网络流、计算几何等高频考点。模板多以清晰注释的 C++ 代码呈现,并配有 Markdown 笔记,便于读者理解原理并快速套用。压缩包共 53 个文件,以 41 份 Markdown 思路笔记为主,另有 11 个可直接运行的 C++ 示例和 1 个 .gitignore 配置文件,整体仅 51KB,轻量易用。目前已有 746 人学习/下载,适合赛前梳理模板体系、查漏补缺的中级及以上竞赛选手;按模块组织、目录清晰,既可用于日常刷题对照,也能在赛时快速定位所需模板,是一份实用性强、覆盖全面的备赛工具包。
1. 竞赛代码模板不是抄板子,是给判题机对口型
竞赛代码模板,说白了就是在 OI、OJ、ACM、PAT、CSP 这类判题环境里反复用到的一套代码骨架。它不是拿来照抄的板子,而是你上考场前就该练熟的口型:读入是什么格式、评测机调的是 main 还是调用你写的函数、边界设多大、用什么编译指令。新手最普遍的误区是把模板理解成现场翻笔记,结果每次都要重新调一遍读入、试一遍边界,时间全耗在接口翻译上。真正有效率的做法是把它固定成肌肉记忆——拿到题先归类,这题套模板里的哪一块,剩余时间都花在推导上。下面不塞给你一份几百页模板库,而是把我在 OI、ACM、PAT、CSP 和常见 OJ 刷题与机试准备里反复验证过的核心套路拆开讲:板子怎么写、参数怎么调、哪些坑最容易让你翻车。
2. 模板地基:输入输出加速与快读快写的参数细节
先说结论:判题环境里,cin/cout把同步关掉之后的表现足够应付大多数题目。剩下那部分卡 IO 的题,基本出现在 OI 的大数据点和部分老 OJ 上——比如杭电 OJ 的图论题读十万行边表,不做 IO 优化就会输在起跑线。模板第一层解决的就是“数据进得来、结果出得去”:万能头文件、快读、以及main开头两行加速。这三个东西各有自己的坑,下面拆开讲。
2.1 bits/stdc++.h 能用,但提交前要确认三件事
很多 OI 选手和 ACM 入门教程第一个教的就是#include <bits/stdc++.h>,它把标准库里能引的头一次性拉进来,比赛时省去翻头文件的时间。在 GNU 系编译器下它真实存在,NOI Linux、多数 OJ 的 G++ 环境都能过。但它不是标准 C++ 头,换到 Clang、MSVC,或者某些采用严格标准检查的评测机,直接编译失败。我见过 PAT 和 CSP 模拟平台上有人因为bits/stdc++.h吃 CE,也见过校内 OJ 的老编译器把它当成普通头文件去搜目录然后报错。
// 不要只依赖 bits/stdc++.h,备一份手动头文件列表 // OI / ACM / PAT / CSP 常见的 G++ 环境下这段足够用 #include <cstdio> #include <cstring> #include <cstdlib> #include <algorithm> #include <cmath> #include <iostream> #include <vector> #include <queue> #include <stack> #include <map> #include <set> #include <string> #include <sstream> using namespace std;这套手动头文件的好处是兼容性宽,从郑州轻工业大学 OJ 到东方博宜 OJ 这类校内平台都能直接过旧标准。bits/stdc++.h的另一个副作用是拖慢编译,大数据题影响不大,但本地反复调试时,编译时间累积起来很磨人。建议你自己写代码时用手动头文件,只在比赛环境明确支持时切换成万能头,不要在模板里写死一个。
2.2 手写快读:判题机读十万个数时的生存技能
快读的原理绕开cin和scanf的流解析,直接用getchar逐字符拼数字。字符到数字的转换是纯内存操作,省掉格式解析和类型检查,所以能快不少。下面这段是我在 ACM 模式和 CSP 真题里常用的整型快读,负数和多组数据都能处理。
int read() { int x = 0, f = 1; char c = getchar(); while (c < '0' || c > '9') { if (c == '-') f = -1; // 处理负数,记录符号 c = getchar(); } while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); // 逐位累加 c = getchar(); } return x * f; }调用时直接int n = read();。注意第一个循环会跳过所有非数字字符,所以读入文件里混着空格、换行、制表符都不影响。第二个循环遇到第一个非数字字符就停,正好卡在下一个空格或换行前面。这个函数读不了long long,如果题目数据范围超过int,把x改成long long、返回值也改成long long就行。极端情况下,比如 OI 的巨量输入,getchar版本还不够,需要用fread一次读一大块进缓冲区再逐个解析,那是性能极致方案,普通机试和大部分 OJ 题用不到。
2.3 ios::sync_with_stdio(false) 开启后的三个后悔药
cin/cout慢的根源是它要和 C 标准库 IO 做同步,保证交替使用cin和scanf时数据不混乱。在main开头写两行,能把这个包袱卸掉:
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); // ... 正常写代码 }这里有两个参数容易害人。第一,sync_with_stdio(false)一旦开启,同一条代码里就不要混用printf和cin,也不要混用scanf和cout,否则缓冲区不同步,输出顺序和内容都会变得随机,这是经典的玄学报错,看起来像数据错乱实际是 IO 打架。第二,cin.tie(nullptr)解除的是cin与cout的绑定关系。默认情况下cin每次准备读入前会先刷新一次输出缓冲区,这句话取消了cin和cout的绑定,所以使用cin和cout时请一律用'\n'而不是endl,结尾及时flush即可。
第三个坑是关闭同步后,scanf的格式化读入就废了。比如 PAT 乙级里经常要求一行读入三个带特殊分隔符的数据,有些人习惯用scanf("Case #%d: %d %d", ...)这种格式化写法。用了sync_with_stdio(false)之后,再写这种代码就是给自己埋雷。如果确定要用printf/scanf,那就放弃这两行加速,用std::ios自带的格式解析也能处理很多需求。我一般的原则是:一个程序里只认准一种 IO 方式,要么全cin/cout,要么全scanf/printf,绝对不混着用。
3. 必背算法模板:二分边界、最短路与快速幂的落地参数
算法模板不是越多越好,而是要在“写得快”和“写得对”之间取平衡。我在 ACM 入门阶段抄过一整本模板,最后发现考场上真正反复用的就那几类:二分、最短路、快速幂、并查集、线段树,再加上字符串哈希。这一章挑三个最容易写错参数的展开,每一个都把边界和溢出讲到能直接复现的程度。
3.1 二分查找的闭区间与左闭右开:把 lower_bound 写稳
二分的坑不在思路,在边界。while(l < r)还是while(l <= r),r = mid还是r = mid - 1,每次写都要重新较劲,说明你还没有固定一套写法。我建议把“找第一个大于等于 target 的位置”这个场景固定成模板,其余变体都从它推。
// 标准 lower_bound:返回第一个 >= target 的位置,不存在时返回 n int lower_bound_pos(vector<int>& a, int target) { int n = (int)a.size(); int l = 0, r = n; // 左闭右开区间 [l, r) while (l < r) { int mid = l + (r - l) / 2; // 用减法代替加法,防止 l+r 溢出 if (a[mid] >= target) { r = mid; // 左边还可能有答案,保留 mid } else { l = mid + 1; // mid 太小,直接排除 } } return l; }关键参数是mid的计算:l + (r - l) / 2而不是(l + r) / 2,因为后者在l和r都是大正数时可能溢出int。另一个关键是左闭右开区间里r初始等于n,这样“找不到目标”时返回的l自然落在数组末尾,不需要额外判断。如果想把题目改成“找第一个大于 target 的位置”,把>=换成>就完了;想改成“找最后一个等于 target 的位置”,用这个模板拿到下界后往前推一个判断即可。STL里自带lower_bound,但手写模板的意义在于应对代码被禁用 STL 的老 OJ,以及在面试手撕时不被边界问题卡住。
3.2 堆优化 Dijkstra:邻接表、优先队列和 INF 的配对关系
最短路的模板几乎每场比赛都会碰到,堆优化 Dijkstra 是稠密图和稀疏图里最稳定的选择。写这个模板时最值得注意的不是优先队列的用法,而是INF这个常量的选择和long long的距离数组。
const long long INF = (1LL << 62); vector<pair<int, long long>> g[100005]; // 邻接表:<终点, 边权> long long dist[100005]; bool vis[100005]; void dijkstra(int s, int n) { for (int i = 1; i <= n; i++) { dist[i] = INF; vis[i] = false; } priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int>>> pq; dist[s] = 0; pq.push({0LL, s}); while (!pq.empty()) { pair<long long, int> top = pq.top(); pq.pop(); long long d = top.first; int u = top.second; if (vis[u]) continue; // 每个点只在第一次出队时展开 vis[u] = true; for (auto e : g[u]) { int v = e.first; long long w = e.second; if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } }INF用1LL << 62是有讲究的:它足够大,能覆盖题目里最坏十万个点、十条边的累加和,又不会大到加一个边权就溢出long long。vis数组负责跳过已经确定最短路的节点,防止无效更新。优先队列里存的是pair<long long, int>,第一维距离,第二维节点,堆顶永远弹出当前最小的(dist, v)。如果你看的模板把pair写成pair<int, int>,在边权较大时一定换成long long,否则十个点全挂,这是 ACM、CSP 真题里血泪教训最多的点。
3.3 快速幂模板:mod 为零、1LL、溢出三件套
快速幂本身不难,难在细节。下面是竞赛常用版:
long long qpow(long long a, long long b, long long mod) { long long res = 1 % mod; // mod 可能为 0,返回 0 而不是 1 while (b) { if (b & 1) { res = res * a % mod; } a = a * a % mod; b >>= 1; } return res; }res = 1 % mod是第一个防呆点:当mod是 1 时,任何数对 1 取模都是 0,写成res = 1会返回错误答案。第二个防呆点在a * a,当a和mod都接近long long上限时,a * a会溢出,正确做法是先用__int128承接中间结果,或者在乘法前判断a > mod先取模。第三个点是必须在返回值上统一类型:底数、指数、模数里只要有一个超过int范围,全部改成long long。这个模板在矩阵快速幂里同样适用,把res换成单位矩阵、a换成矩阵、乘法换成矩阵乘法即可,参数检查的逻辑不变。
4. ACM 模式与核心代码模式:在不同 OJ、PAT、CSP 之间切换套路
做题平台多了以后会碰到一个很尴尬的问题:在 LeetCode 上刷题从来不用管输入输出,换到杭电 OJ 上就得自己写全套 main。这两年刷题圈子里对“ACM 模式”和“核心代码模式”讨论得很多,华为 OD 机试也明确按 ACM 模式操作,不少人在笔试时翻了车,就是因为只练过一种模式。这一章专门说模式切换。
4.1 完整程序 vs 核心函数:OJ 刷题和华为 OD 机试的输入差异
传统 OJ 是“完整程序模式”,你提交的是带main的整个文件,评测机把测试数据喂给 stdin,它从 stdout 读结果。而核心代码模式是评测机已经帮你写好了输入,直接调用你实现的函数,常见于 PAT 的部分题目、CSP 认证的部分模拟平台,以及 LeetCode 风格笔试题。ACMer 习惯的写法是前者。
// ACM 模式:完整程序,自己在 main 里做 I/O // 典型场景:杭电 OJ、传统 OJ、华为 OD 机试 #include <iostream> #include <vector> using namespace std; int main() { int n; while (cin >> n) { // 多组数据,读到 EOF 结束 vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; long long sum = 0; for (int v : a) sum += v; cout << sum << '\n'; // 每组结果单独一行 } return 0; }// 核心代码模式:无需写 I/O,评测机把参数传进来 class Solution { public: int solve(vector<int>& nums) { long long sum = 0; for (int v : nums) sum += v; return (int)sum; } };两种模式的切换要点:核心代码模式不用处理多组输入和 EOF,但必须严格按题目给的函数签名来写,返回值类型、参数传引用还是传值都不能改;ACM 模式则要注意多组输入杀到 EOF,漏写while(cin >> n)就会只跑第一组样例。华为 OD 机试的输入格式往往和传统 OJ 不完全一致,常见的是第一行给几个数,后面跟若干行,中间用空格和逗号混着分隔。写代码前先把输入读进来打印一遍,确认读入逻辑正确再往下做。
4.2 PAT 乙级常见的读入套路:EOF、行尾和格式化输出
PAT 乙级题目对输出的格式化要求极其严格,多打一个空格都是 WA。读入端也有它独特的习惯:很多题目以“多组输入,读到 EOF”作为前提,而不是先给一个测试样例数。还有一类题是“第一行给 N,后面 N 行每行一个操作”,这时候一定要防住换行符残留。
int n; cin >> n; // 先读整数 string line; getline(cin, line); // 这一步会吞掉换行符,不是目标数据 getline(cin, line); // 真正的第一行数据很多人在 PAT 上第一次翻车就是因为这个空行。cin >> n只读数字,后面的换行符留在输入流里,第一次getline读到的必然是空串。解决方式要么在数字后额外调一次getline吃掉换行,要么用cin.ignore(numeric_limits<streamsize>::max(), '\n')。PAT 的题目还有一个特点是输出末尾常要求不能有多余空格,我会在最后一段模板里留一个first标志变量,第一个元素前不打空格,之后的元素前打空格,这样能绕开绝大多数格式化问题。
4.3 CSP 字符串题的 getline + stringstream 组合拳
CSP 认证的很多题目会给一大段多行字符串,要求按行解析,再用空格或逗号切分成 token 序列。直接cin >> s只能读空格分隔的单词,遇到“一行里可能包含多个空格和多个 tab”的情况就歇菜。我常用的组合是getline读整行,stringstream做切分。
#include <sstream> vector<string> split_line() { string line; getline(cin, line); // 一次读整行,保留内部空格 stringstream ss(line); vector<string> tokens; string token; while (ss >> token) { // 按空白切分,方便又稳 tokens.push_back(token); } return tokens; }stringstream会把连续空格、制表符都按一个分隔符处理,不需要手动split。如果题目要求用逗号分隔,先手写一个循环替换逗号为空格,再继续走stringstream。比写复杂的状态机快得多。CSP 题目另一特点是数据规模给得实在,不会故意刁难 IO,但读入方式错了会在整行解析时把后续所有数据搞乱,所以我在每场模拟赛前都先把这段split_line在编辑器里敲一遍,保证没有语法层面的生疏。
5. 模板翻车排查:五个高频现场和它们的仪表盘信号
模板不是背下来就能稳过,真正的问题永远发生在各种平台细节里。下面是五条覆盖“本地编译、样例通过、大数据全挂、递归爆栈、类型溢出”的踩坑记录,每条按现象、原因、解决三个步骤写,方便你对着排查。
5.1 本地编译通过,提交 CE
现象:本地 G++ 编译一切正常,提交到 OJ 或 PAT 报编译错误。原因有三类,最常见的是评测机编译器版本较旧,比如本地用的 C++17 写auto [a, b] = pair_val结构化绑定,评判端只支持 C++11,直接语法错误;第二类是你用了bits/stdc++.h,碰到 GCC 版本过老或编译器非 GCC 的环境会找不到这个头;第三类是某些校内 OJ 使用 VC++ 编译器,不支持%d之外的某些格式化写法。解决:提交前查 OJ 首页支持的编译选项,优先用 C++11/14 的保守语法,避免依赖 C++17 特性;同时把万能头替换为手动头文件列表。如果编译错误信息看一眼是头文件问题,立刻换手动 include 重交一次。
5.2 样例全对,换大数据 TLE
现象:样例跑得快如飞,交上去 Time Limit Exceeded。原因通常不是单点的常数慢,而是复杂度算错了。比如O(n^2)的暴力过了样例里的小 n,换到 CSP 认证的 10^5 量级就直接卧倒。解决思路是回到题目规模做一次心算:10^7是 1 秒上下,10^8是 2 秒之上的极限,10^9必挂。如果算出来自己的代码在10^7以上,要么从暴力改成二分、哈希、并查集等优化结构,要么做剪枝。换句话说,TLE 不是优化出来的,是设计阶段就该算出来的。
5.3 递归爆栈
现象:递归深度上万层,本地偶尔过,OJ 上跑着跑着 Segmentation Fault 或者 Runtime Error。原因:评测环境的栈空间有限,OI、OJ 的递归栈一般也就几 MB,深度超过10^5就会爆。比如深度优先遍历一条链状的图,递归函数一层层压栈,每个栈帧还带着局部变量,直接顶穿。解决:把递归写成显式栈循环,或者用vector模拟栈的入出。另一个临时方案是加大编译器的栈大小,比如 G++ 加-Wl,-stack,size,但提交时大多数 OJ 不让你改编译参数,所以把核心函数从递归改成迭代才是一劳永逸的。
5.4 int 乘法溢出
现象:小数据全对,数据一大输出就开始随机飘负数或者错误大数。原因是题目里两个数的乘积超过int上限2^31-1。典型的坑发生在“求两点之间的距离平方和”这类题,坐标是10^5级别,平方后就是10^10,int存不下。解决:所有可能参与乘法的中间量都提升成long long,乘法时在第一个量后加1LL,比如1LL * a * b,强制走long long运算。另外,INF常量也要用long long版本,不能在long long距离数组里填0x3f3f3f3f——那只配 int 数组使用。
5.5 getline 接在 cin>>n 后面读到空行
现象:写了cin >> n后接getline(cin, s),读到的 s 是空字符串,后续所有逻辑全部错位。原因是>>运算符会在目标变量填充后停下来,但换行符还留在输入流里,getline默认读到换行符为止,于是吃到的第一样东西就是换行符本身。解决:在>>和getline之间加一行cin.ignore(numeric_limits<streamsize>::max(), '\n'),把缓冲区里的换行清干净。这个坑在 PAT 和 CSP 的字符串题里几乎必出现,建议直接写进你的核心模板注释里,不要到考场上再来回忆。
6. 模板到肌肉记忆:对拍脚本与模板目录组织
最后聊一个常被忽略的验证方法:对拍。很多人的“模板练习”就是反复抄,抄到自己信手写出来就算完。真正有效的是拿随机小数据和暴力写法对拍,用脚本批量验证模板的正确性。我自己会在比赛前把模板目录按功能拆开,每次只动一根手指。
# 对拍脚本 stress.sh:随机数据验证 sol.cpp 与 brute.cpp 的输出 for i in $(seq 1 200); do ./data > in.txt # 数据生成器输出到 in.txt ./brute < in.txt > out_brute.txt ./sol < in.txt > out_sol.txt if ! diff -q out_brute.txt out_sol.txt > /dev/null; then echo "第 $i 组数据不一致" break fi done这里data.cpp是随机数据生成器,brute.cpp是确保正确的暴力实现,sol.cpp是你要测试的带模板代码。用法上先编译三个可执行文件,再跑脚本。它对二分、最短路、字符串哈希这类正确性敏感的模板特别有用,一次对拍能筛掉你在边界上漏掉的百分之八十问题。模板目录我一般按00_head、01_io、02_math、03_graph、04_string这样组织,每个文件只放一个独立函数,避免“全合在一起改一处牵连别处”的窘境。
刷题这件事,我最大的体会就是不要高估临场发挥,也不要低估肌肉记忆。每次模拟赛前,我把最常用的四五个模板盲打一遍,打到不会卡壳,再把踩过的坑当成 acm 日记记下来。模板管理本质上是风险管理:把代码里最容易出错的那几步拆出来反复练,练到条件反射。希望帮到你。
本文还有配套的精品资源,点击获取