
简介编程竞赛中OI、OJ、ACM、PAT、CSP 等赛事要求选手在有限时间内写出正确高效的代码一套经过实践检验的模板库能显著提升解题速度。这套模板合集正是为此整理覆盖数据结构、动态规划、贪心、图论、数学、字符串等高频考点从入门到进阶均有涉及。压缩包共 53 个文件以 41 个 Markdown 笔记为主体并附 11 个 C 可直接运行的示例和 1 个 Git 忽略配置Markdown 按「基础算法、动态规划、数学、字符串、图论、数据结构」模块划分便于按专题检索C 示例则演示了快读、整数取整、区间数字统计等易错写法。包体仅 51KB轻量便携可离线阅读或同步至云笔记。目前已有 746 人学习下载适合从入门到冲奖各阶段的读者既能帮助新手建立算法知识框架也能作为老选手赛前速查手册特别是逆元、欧拉函数、强连通分量、最小生成树、网络流等进阶内容可减少重复造轮子的时间损耗让思路更集中在题目本身。1. 为什么每个认真刷题的人都该维护一套自己的代码模板如果你在 OI、OJ、ACM、PAT、CSP 任何一个赛道上坚持刷题超过三个月大概率会有这种体验一道题的核心思路五分钟就想通了结果从敲第一行 include 到写完输入输出解析又过去了十分钟。等到提交时告诉你“运行超时”或者“段错误”你改了半天才发现是快读没写、数组开小了。这不是你水平不行是你手里缺一套属于自己的“做题脚手架”——就是标题里说的题目常用代码模板。所谓模板不是让你去背网上的大段代码而是把那些每次做题都会重复出现的结构提前写好、调试通、验证过让它们变成你的肌肉记忆。这套模板解决的是“把想法变成能跑的代码”这段路的速度和稳定性问题面向的是正在备赛 OI/ACM 的学生、准备 CSP 认证和 PAT 考试的考生以及所有需要应对在线评测OJ环境做题的开发者。标题里那些大写缩写看着吓人落到日常就是一个字练。练得有章法比练得时间长更重要。2. 模板的底层逻辑OJ 到底怎么运行你的代码2.1 在线评测系统的“黑匣子”机制要写好模板首先得知道你交上去的代码经历什么。无论是 OI 用的 NOI Linux、ACM 的 HDU/POJ/Codeforces 类平台还是 PAT 的浙大评测系统、CSP 认证的考试环境它们的流程都一样你的源码被编译成可执行文件系统喂入一组预先准备好的输入文件在限定时间和内存内捕获你的程序输出然后和标准答案逐字节比对。这里面有个容易忽略的点OJ 系统一般不解析你的代码结构它只看最终可执行文件的行为。这一机制决定了模板的第一原则代码跑得快、输出格式严格正确比代码写得“优雅”重要一百倍。比如 PAT 的题目经常有“格式错误”这种处罚你多打一个空格、少换一次行都会挂掉。ACM 赛制下则是任何一次错误提交都有罚时。CSP 认证则是五道题、四个半小时每题按子任务给分部分通过也有分。这些赛制差异直接影响模板的侧重点。2.2 输入输出模板的生死线所有 OJ 场景里最常见的翻车点就是输入输出。C 的 cin/cout 默认是与 C 标准 IO 同步的这意味着它每读一个变量都要检查与 stdio 的缓冲区是否一致这个开销在数据量上百万级别时会被放大到不可接受。常见做法是关同步这是模板里的第一行“保命代码”。#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); // 你的业务代码 return 0; }这段代码的关同步逻辑要讲清楚ios::sync_with_stdio(false)切断 cin/cout 与 stdio 的同步让 C 流独立缓冲cin.tie(nullptr)解除 cin 和 cout 的绑定避免每次输入前强制刷新输出缓冲区。做完这两步cin/cout 的速度大约能提升到与 scanf/printf 接近。但要注意关同步后不能混用 cin 和 scanf、cout 和 printf否则数据会错乱——那正是模板最隐蔽的坑。对于输入量特别大的题目我一般会直接用快读模板而不是依赖 iostream。快读模板的核心逻辑是手写一个read()函数用getchar()逐字符解析整数代码也不长static inline 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; }这个模板按需记忆它的逻辑说明很直白第一个 while 循环跳过非数字字符并记录负号第二个 while 循环累加每一位数字。要求数据中只有正整数时可以去掉f相关的两行。为什么不用 getchar 直接搞定点数因为浮点数输入在竞赛里不常见真遇到了用 cin 或者库函数更稳。2.3 数组空间预开的哲学OJ 界的经典玄学数组开小了是 RE运行错误开大了是 MLE内存超限。模板帮你解决的是前者后者需要你对自己用的数据结构有把握。常见做法是定义一个全局数组大小按题目约束上限再加一点余量const int MAXN 100000 5; int a[MAXN];加 5 而不是加 1 是因为有些题目会在边界访问时越界比如从 1 开始索引的数组访问到a[n]是合法的但如果你开了MAXN 100000n 恰好等于 100000 时a[n]就越界了。加 5 是个行业共识既不多占内存又稳住了常见越界。注意这个“加 5”不是严谨的防越界方案而是应对多数题目的经验值真正的防越界靠的是循环条件写对。3. 核心算法模板图论、搜索、DP 的骨架怎么搭3.1 图论模板的三种必备姿势图论题是 OI/ACM/PAT/CSP 的高频区但图论题的变化极其丰富模板能帮你的是把存图方式和最短路、最小生成树的骨架固定下来。存图方式我推荐链式前向星——优点是省内存、适用于稀疏图缺点是代码比 vector 邻接表长但比赛时它给你省下的复杂度是实实在在的。struct Edge { int to, next, w; } edge[MAXM]; int head[MAXN], tot; inline void init() { memset(head, -1, sizeof(head)); tot 0; } inline void addEdge(int u, int v, int w) { edge[tot].to v; edge[tot].w w; edge[tot].next head[u]; head[u] tot; }这段模板的参数说明head[u]存储以 u 为起点的第一条边在 edge 数组中的下标next指向下一条以 u 为起点的边所以遍历时是从 head[u] 出发一路沿着 next 走到 -1 为止。addEdge采用头插法新边总是插在链表头部。要从 u 遍历所有邻边写法是for (int i head[u]; i ! -1; i edge[i].next)。最短路模板方面dijkstra 堆优化版是必备它同时服务于正权图的最短路和单源最短路场景。模板骨架里堆用priority_queue边用链式前向星存的结构写出来大概这样void dijkstra(int s) { memset(dist, 0x3f, sizeof(dist)); memset(vis, false, sizeof(vis)); dist[s] 0; priority_queuepairint, int, vectorpairint, int, greaterpairint, int pq; pq.push({0, s}); while (!pq.empty()) { int u pq.top().second; pq.pop(); if (vis[u]) continue; vis[u] true; for (int i head[u]; i ! -1; i edge[i].next) { int v edge[i].to; if (!vis[v] dist[v] dist[u] edge[i].w) { dist[v] dist[u] edge[i].w; pq.push({dist[v], v}); } } } }这里dist初始化用0x3f3f3f3f是个常识这个数约等于 10 亿作为无穷大使用时两个无穷大相加不会溢出 int且 memset 按字节填充时能直接得到这个值——你 memset 0x3f 和 0x3f3f3f3f 的结果是一样的。如果INF用INT_MAX就会存在溢出隐患。模板的参数说明要写清楚pair 排序默认按 first 即距离升序vis标记的是已经确定最短路的节点这个标记不是必须的但能加速代价是多了一个布尔数组的遍历判断。3.2 搜索模板DFS 的剪枝骨架与 BFS 的状态压缩搜索是 OI 入门和 PAT 乙级早期题目的核心DFS 的模板得分场景看用途——排列枚举、组合枚举、图遍历、回溯搜索。一个比较通吃的 DFS 模板骨架是带路径记录的vectorint path; void dfs(int cur, int n) { if (cur n) { // 输出 path 的一种排列/方案 for (int x : path) cout x ; cout \n; return; } for (int i 1; i n; i) { if (!used[i]) { used[i] true; path.push_back(i); dfs(cur 1, n); path.pop_back(); used[i] false; } } }这段逻辑很直白used数组标记元素是否已使用递归深度 cur 达成时输出一组排列回溯时恢复状态。这是排列类题目的标准骨架组合类题目则需要加一个start参数——dfs(cur 1, i 1)的传入方式确保每次只选下标更大的元素避免重复组合。BFS 模板核心是队列和状态数组。有些状态是二维坐标有些是带步数的还有需要压缩成一维整数的状态。常见的 BFS 骨架要能把坐标状态和距离数组统一起来queuepairint, int q; int dist[MAXN][MAXN]; int bfs(int sx, int sy) { memset(dist, -1, sizeof(dist)); dist[sx][sy] 0; q.push({sx, sy}); int dx[] {1, -1, 0, 0}; int dy[] {0, 0, 1, -1}; while (!q.empty()) { auto [x, y] q.front(); q.pop(); for (int d 0; d 4; d) { int nx x dx[d], ny y dy[d]; if (nx 0 || nx n || ny 0 || ny m) continue; if (dist[nx][ny] ! -1) continue; // 已访问 dist[nx][ny] dist[x][y] 1; q.push({nx, ny}); } } return -1; // 无法到达 }这个骨架的关键点是dist 数组同时充当访问标记和距离记录初始化为 -1 表示未访问。用结构体打包坐标也行但 pair 的写法更省空间。移动方向数组写成 dx/dy 而不是多维数组是为了方便改方向数量——八连通就补两对。注意 BFS 题目里 dist 初始化和入队顺序都有讲究先入队的先出队所以第一次到达的就是最短路径。3.3 DP 模板从记忆化到递推的切换套路DP 部分没有统一的代码模板因为状态设计和转移方程每题不同。但有一个通用骨架——记忆化搜索版本它适合状态不好按拓扑序写递推的题代码思维和 DFS 更像long long dp[MAXN][MAXN]; bool vis[MAXN][MAXN]; long long solve(int i, int j) { if (vis[i][j]) return dp[i][j]; vis[i][j] true; if (i 0 || j 0) return dp[i][j] 1; // 边界 dp[i][j] solve(i - 1, j) solve(i, j - 1); // 转移方程按题改 return dp[i][j]; }这段模板的意义是先把搜索框架搭好再改转移方程。记忆化搜索相比递推的优点是省去拓扑排序和数组维度的规划焦虑缺点是递归可能爆栈且常数更大。在 CSP 认证这类不给开大栈的环境里深递归或记忆化搜索是可能溢出的——这是模板选型要考虑的问题。所以递推型的 DP 骨架还是要有的通常是for循环枚举状态维度从边界填上去。DP 模板题背得滚瓜烂熟之后你会发现拿到题第一件事不是想状态怎么设计而是确认维度和边界再把转移填进去。4. 竞赛环境里的“磨刀石”快读、高精度、模运算与常用宏4.1 模运算模板九成以上需要取模的题都用它在 PAT 和 CSP 里凡是结果可能超 int 范围的题基本都会让你取模。取模运算有两个坑一是减法结果可能是负数二是乘法可能溢出 long long。模板要把这两点都包住typedef long long ll; const ll MOD 1000000007; inline ll add_mod(ll a, ll b) { return (a b) % MOD; } inline ll sub_mod(ll a, ll b) { return ((a - b) % MOD MOD) % MOD; } inline ll mul_mod(ll a, ll b) { return (a * b) % MOD; }减法取模的 MOD是必须写的C 里%运算结果符号与被除数一致负数取模会得到一个负数加上 MOD 再取模才能回到正数范围。乘法取模则要求a * b不溢出 long long如果题目给的数在 1e9 量级乘起来是 1e18long long 的极限约 9.2e18所以基本安全再大就得用快速乘。快速乘模板算是进阶内容ll mul_mod_large(ll a, ll b, ll mod) { ll res 0; while (b) { if (b 1) res (res a) % mod; a (a 1) % mod; b 1; } return res; }这个模板的思路类似快速幂把乘法拆成二进制的加法叠加。实际用到的场景不多但大数相乘的题目它能让你的代码不依赖__int128这类编译器扩展提升可移植性。注意这里a 1可能超过 long long 范围但加一个% mod每次都会折回所以逻辑上没问题。4.2 高精度与字符串大数运算的模板高精度加法、乘法是 PAT 乙级和 CSP 入门模拟题的老面孔。通用做法是用vectorint存储每一位倒序存使得进位方便。高精度加法模板长度不长vectorint add(vectorint A, vectorint B) { vectorint C; int carry 0; for (int i 0; i A.size() || i B.size(); i) { if (i A.size()) carry A[i]; if (i B.size()) carry B[i]; C.push_back(carry % 10); carry / 10; } if (carry) C.push_back(carry); return C; }逻辑说明A 和 B 的个位都在下标 0循环每轮做当前位的加法并保留进位最后若进位非零补一位。这里不判断 A/B 长度谁大直接靠||条件遍历到较长者结束省去前置对齐代码。高精度乘法要靠双重循环逐位相乘再加进位模板略长但结构固定背下来不亏——大数阶乘这类题能直接改。4.3 常用宏与调试期后遗症比赛模板里的宏和 define 要克制。#define int long long这种写法在正式比赛里能救急但会造成两个问题一是 main 函数的返回类型必须是signed int而不能是 int否则与宏冲突二是把 int 替换为 long long 后内存占用可能翻倍MLE 风险上升。我的建议是不要在模板里默认开它而是按题目约束判断——需要 64 位就用long long显式声明需要频繁书写时就写typedef long long ll。调试用的#define dbg和输出调试信息在 OJ 上是翻车重灾区。很多人本地调试时在代码里加了cerr输出交上去忘了删——OJ 捕获的是 stdoutcerr 输出到 stderr 不比对但不删会拖慢运行时间部分测评系统会把 stderr 回传导致 OLE/信息泄露。CSP 认证的环境不限制 stderr 大小但 ACM 赛制会所以模板里我默认不带上任何调试输出代码调试完毕才 copy 到提交区。5. 避坑指南模板写到能用只是第一步避开这些坑才算合格5.1 多组输入的陷阱EOF 处理与 resetsPAT 和部分 OJ 的题目有这样的输入描述“输入包含多组测试数据每组占一行处理到文件末尾。”常见错误写法是只读一组就看答案交上去直接 WA答案错误。标准模板要能正确处理 EOFint main() { int a, b; while (cin a b) { cout a b \n; } return 0; }逻辑说明cin a b这个表达式的返回值是流对象本身流在读到 EOF 时会转换为 false循环自然终止。注意这里不要用while (!cin.eof())作为条件——因为 EOF 标志在最后一次成功读入之后才置位按这个写法会多循环一次且可能读到未初始化变量。这是个非常常见的翻车点尤其在处理“输入以某个特殊值结束”和“输入到EOF结束”这两类题时容易混淆前者是while (true)加break后者才是 EOF 循环。5.2 四舍五入与浮点数精度OJ 的标准答案比对对浮点数有容差但 PAT 的题目经常要求输出到小数点后两位用printf(%.2lf, ans)就足够了。这里有一类著名玄学坑printf的四舍五入依赖当前编译环境和 IEEE 754 表示有些数值在二进制下不能精确表示比如2.675用 printf 保留两位会输出2.67因为 2.675 在内存里实际是 2.6749999999999998。遇到这种题目常见做法是输出前ans 1e-9或1e-8防止这种精度导致的向下舍入。模板里如果涉及浮点数输出你要么按题目给的规则调整精度要么就告诉自己“编译器四舍五入靠不住”——在算法竞赛里能用整数运算的就别用浮点数这是减少这类问题最釜底抽薪的办法。5.3 边界状态下标从 0 还是从 1 开始每套模板内部要统一。链式前向星的 head 初始化是 -1那么遍历的终止条件是i ! -1如果用 0 作为空标记那么 head 初始化是 0终止条件就是i ! 0。这两种写法之间切换很容易把人搞懵尤其是两个模板拼一起用的时候。我建议模板里只用一个约定数组下标全部从 0 开始空标记用 -1。前缀和、差分数组这类则经常需要从 1 开始因为方便处理边界。混用不致命致命的是频繁切换导致写循环时少写一个-1或1这种错误极难定位因为答案局部正确。5.4 爆栈与递归深度的现实边界DFS 在 OI 时代的题目里递归深度可能达到十万级在 Linux 环境默认栈约 8MB 的情况下没问题但在 Windows 本地环境跑同样的代码就可能直接退出不报错。CSP 认证的考试系统栈也比较紧张所以模板里遇到不确定深度的 DFS常见做法是用栈模拟递归或者在主函数里手动扩展栈空间——但要注意这不是标准平台的 API。模板里我一般不用手动扩栈而是预判递归深度超过 1e5 的深搜就换非递归写法或者用 BFS 代替 DFS。这个取舍在“最大连通块”这类题目里尤其重要因为数据强时 DFS 就是会爆栈。5.5 头文件与编译环境的“版本敏感”#include bits/stdc.h是竞赛圈标配但它在 GCC 上是可用在其他部分编译器或某些在线平台可能没有。PAT 的评测环境支持它CSP 认证环境也支持但如果你在牛客网这种平台测过会发现有的老编译器不行。模板里保留它是为了做题效率但要知道自己的代码依赖的是这个“万能头文件”的红利。上次有位朋友在本地 VS Code 配的 GCC 编不过这个头耽误了半天——原因是他的 MinGW 没装完整。这个问题解决很简单装完整版 MinGW 或者老老实实用iostream、algorithm、vector等具体头文件逐个 include。6. 把模板变成自己的本领用题目反推模板更新维护模板不是写完一遍就完事是需要持续迭代的工作。我对模板的管理方式是按场景分文件保存一个data_structure.cpp放图论和树结构一个algorithm.cpp放手写算法骨架一个io_template.cpp放输入输出相关的骨架。每次遇到一次新题目如果发现某个解法里有一段代码不是背下来而是现场想出来的就去翻翻模板里有没有可以补进去的部分。比如第一次遇到“二维差分”题时我现场推了半天公式刷完题之后就把二维差分的 add、前缀和重构两个函数补进了模板下次同类型题直接抄改写参数就行。做题与模板的配合也有讲究。我的个人习惯是拿到题先不翻模板用白纸推 10 分钟状态和边界确认思路后再打开模板做骨架填充。模板不是让你跳过思考过程的捷径而是省去你重复写memset、重复写快读、重复写建图结构体的无效时间。有一次刷 PAT 甲级题一道描述很长的模拟题我靠模板里的读入和输出骨架直接过了一个多小时全部花在理解题意和设计数据结构上——这正是模板该有的姿态默默在后台托底不该抢戏的时候绝不跳出来打扰你。建议准备一个笔记把每次翻车的原因记下来。我自己有个习惯每道题如果卡在模板相关的问题上超过 20 分钟就把原因和解决写进一个独立的 debug 笔记文件比如这里记过“用 printf 输出 long long 时必须写 %lld 不能写 %d”“sort 的 cmp 可能对相等元素返回值不稳定导致 RE”“memset 只能对 0、-1、0x3f 这三种常用值生效”。这些血泪经验积累一年之后你的模板就不再是公共代码的拼凑而是你自己甄别过取舍过的兵器库。希望这些梳理能帮你在 OI、OJ、ACM、PAT、CSP 的征途上把每一段重复的代码都变成可以忘记的细节把精力留给真正需要思考的地方——毕竟做题的快乐不该被编译器玄学和 IO 细节消耗干净祝你刷题顺利。本文还有配套的精品资源点击获取