
在算法竞赛这条路上我前前后后折腾过不少项目但真正让我觉得“这钱花得值、时间没白费”的反而是这个我自己从零维护的t3code。它不是某个知名开源框架也不是能直接跑出结果的现成工具而是一套完全围绕我个人习惯与竞赛需求打造的代码模板库与训练闭环系统。核心要解决的只有一个问题如何在赛场上把“想清楚”和“写出来”之间的损耗降到最低。如果你也经常在比赛中因为手速不够、细节写错、板子临时翻车而丢分那这篇聊聊 t3code 的思路、结构和具体落地过程一定对你有参考价值。t3code 不是什么“银弹”它更像是一座属于我自己的武器库平时维护赛时取用。适合的人群也很明确——每天要刷题、有固定参赛节奏、又不想在赛场上从零敲代码的算法选手。尤其当你发现“这道题我明明会但就是没写完”出现的频率越来越高时t3code 这类项目就是你最需要的解药。它的价值不在于代码量有多少而在于组织方式和复盘机制。1. 为什么非要有自己的 t3code而不是直接抄模板1.1 直接套模板的第一个坑模板是别人的思路不是你的很多人觉得模板无所谓能从网上抄一份“竞赛模板大全”就完事。我最初也是这个想法直到有一次打线上赛需要用到带 lazy 标记的区间加乘线段树。网上那套模板写得确实漂亮但我用的时候整整卡了 15 分钟先是变量名绕来绕去然后发现自己平时习惯的写法是“先 pushdown 再计算”但那套模板是“先计算再 pushdown”。逻辑本身没问题可我的肌肉记忆和它打架结果就是改 BUG 改到头皮发麻。t3code 给的第一个答案就是模板必须过自己的手用自己最舒服的命名和顺序。别人代码里的update(int p, int l, int r, int ql, int qr, int v)在我这里一定写成add(node, nl, nr, ql, qr, val)而且每个变量的含义我都标注在注释里。因为只有亲手敲过一遍你才知道这段代码在什么边界条件下会炸也才知道它和你惯用的存储方式比如 1-indexed 数组、结构体封装是否契合。1.2 赛场上时间不是线性分配的而是“抢”出来的真正打过几场高强度比赛的人会明白比赛时间从来不是均匀分配到四道题上。通常前 60 分钟你在快速解决简单题和中等题后 60 分钟可能全部耗在一道数据结构上。如果你平时没有维护一个结构清晰的板子仓库那么到了赛场上你就只能靠记忆去快速重构模板代码。记忆这个东西在心跳加速的时候特别不可靠。你会突然想不起上次 AC 时那个二分边界到底怎么写在check(mid)里也会忘记 Tarjan 缩点之后栈里该不该pop()。t3code 的设计目标非常直白把赛场上所有“回忆正确代码”的过程全部变成“复制并微调”的过程。只要你在平常刷题时把每一个 AC 的经典代码片段都沉淀进去那么你比赛时手指敲击的就不是陌生代码而只是在自己看过的内容上做参数调整。这就是为什么 t3code 表面看只是代码库实际确实是一个“第二大脑”式的外置存储。1.3 核心关键词模块化、可检索、可回滚我从一开始就没把 t3code 定义成“大而全”的模板总集而是把它拆成模块每个模块只解决一类问题。这样有三个好处模块化让每一个板子都能独立测试和部署可检索让你在 10 秒内定位到某个模板在仓库中的准确位置可回滚则保证你每次更新模板时都能对比新旧版本不会因某次“乱优化”反而把稳定版本改坏了。具体来说t3code 的仓库结构是这样的/core最基础、最常用、全局通用的代码片段比如快读快写、gcd/lcm、素数筛。/ds数据结构专题包括并查集、线段树、树状数组、莫队等。/graph图论专题包括最短路、最小生成树、网络流、Tarjan 全家桶。/math数论与组合数学专题包括逆元、快速幂、FFT、矩阵快速幂。/geo计算几何专题以点、线、圆、凸包处理为核心。/strings字符串处理包括 KMP、AC 自动机、后缀数组、Manacher。/dp动态规划专题常见状态转移模板、斜率优化、四边形不等式。/misc其他零碎但高频用到的东西比如随机化、模拟退火、对拍脚本。每个模块下不只是单独的文件还带一个README.md记录每份模板对应的例题、适用条件、复杂度分析、以及与同类模板的对比。这样一份仓库维护好了不只是比赛能用平时复习知识点时也能起到提纲挈领的效果。2. 核心代码怎么组织才配得上训练流的高频调用2.1 快速输入输出你省下的不是几毫秒而是一次次 RE 的风险许多初学者总觉得快速读写是“花架子”但在 t3code 的第二版整理中我把fast_io放在/core的第一个位置。原因很简单很多比赛环境用cin/cout不关同步确实会超时而且还有潜在的输入解析不一致风险。相比之下一个稳定、经过长期比赛验证的快速读入模板能够消除输入这一环节的不确定性。我平时用的快读函数很简单核心思路就是自己写getchar()循环按位构建整数inline int read() { int x 0, f 1; char ch getchar(); while (ch 0 || ch 9) { if (ch -) f -1; ch getchar(); } while (ch 0 ch 9) { x (x 1) (x 3) (ch ^ 48); ch getchar(); } return x * f; }可能有人会觉得这样写不如scanf简洁但它有个隐藏优势当输入里包含可能的行尾空格、回车换行或 EOF 边界时这套逻辑的表现完全确定绝无平台差异导致的 RE。我遇到过不止一次在家里本地跑明明没问题一上评测机就错最后排查发现是cin和scanf混用导致缓冲区状态异常。用 t3code 里的统一快读函数后这类问题基本绝迹。2.2 数据结构模板重点不只是“能跑”更是“能在高压下不错”数据结构是赛场的半壁江山也是 t3code 中我最花心思的板块。以线段树为例一份好的板子必须做到三点单一数据源、宏定义与函数体分离、边界判定完整。我写的区间求和线段树模板大致长这样template typename T struct SegTree { vectorT sum, tag; vectorint lc, rc; int n; SegTree(int n) : n(n) { sum.resize(n 2); tag.resize(n 2); } void pushup(int p) { sum[p] sum[p 1] sum[p 1 | 1]; } void pushdown(int p) { if (tag[p]) { tag[p 1] tag[p]; tag[p 1 | 1] tag[p]; sum[p 1] tag[p]; sum[p 1 | 1] tag[p]; tag[p] 0; } } void update(int p, int l, int r, int ql, int qr, T val) { if (ql l r qr) { sum[p] val; tag[p] val; return; } pushdown(p); int mid (l r) 1; if (ql mid) update(p 1, l, mid, ql, qr, val); if (qr mid) update(p 1 | 1, mid 1, r, ql, qr, val); pushup(p); } T query(int p, int l, int r, int ql, int qr) { if (ql l r qr) return sum[p]; pushdown(p); int mid (l r) 1; T res 0; if (ql mid) res query(p 1, l, mid, ql, qr); if (qr mid) res query(p 1 | 1, mid 1, r, ql, qr); return res; } };这一版把tag当作懒标记用pushdown下传区间增量。有人会问为什么pushdown里要把tag往下传而不是往上合并因为懒标记的核心语义就是“当前节点的信息已更新但子节点还未同步”。如果你在修改区间完全覆盖当前节点时顺便清空了tag却在查询时发现子节点的 sum 还是旧值那就必然会出现错误。t3code 这一版模板的关键就在于它严格保证任意时刻查到的节点之和都等于真实和当且仅当你完整调用update后pushdown与pushup成对出现。2.3 图论与数学专题的搭配离散化、逆元与预处理的边界图论和数学是 t3code 里最容易“堆代码”的模块也是最容易变成“垃圾回收站”的模块。我的做法是每个模板都绑定一个具体的例题场景绝不能脱离背景单独抽离代码。比如最短路模块Dijkstra 有普通优先队列版和配对堆版本SPFA 则只标记“负权图专用”。因为普通竞赛题里Dijkstra 是绝对主力SPFA 在有负环或者特定图结构下容易退化到 O(VE)如果你平时不写清楚“此板子只用于哪种情况”赛场上十有八九要误用。t3code 的数学模块里逆元、组合数预处理会统一用C(n,k)函数封装内部实现乘法逆元时质数模数下走费马小定理快速幂非质数时则用扩展欧几里得这也是需要根据题目条件快速切换的点。const int MOD 1e9 7; long long modpow(long long a, long long b) { long long res 1; while (b) { if (b 1) res res * a % MOD; a a * a % MOD; b 1; } return res; } long long inv(long long x) { return modpow(x, MOD - 2); }有人说这是老生常谈但真的到了赛场上如果你连“快速幂的指数类型是不是 long long”、“模数是不是质数”这些问题都需要现场做二次判断那时间损耗是非常可观的。t3code 的做法是每一份模板的头部注释里都写明适用约束。比如上述inv(x)就强制要求 MOD 是质数如果不是质数就直接报错或在注释里标红。这样我使用时不需要反复审视代码逻辑只需要扫一眼注释就行。3. 实操落地从搭建目录到形成训练闭环的全流程3.1 第一步初始化仓库与统一代码风格t3code 的搭建流程来源于我自己长期试错后的总结。先说初始化我的做法是直接在本地建一个 Git 仓库然后再考虑同步问题。一开始没有必要一定要推到远端但本地仓库一定要建因为后面做版本回滚的时候特别方便。我建议的第一步是把.clang-format或命名规范先定下来。有人会觉得代码风格这件事无所谓但我实测下来风格统一能够显著缩短临时查板子的认知负担。我采用的规范如下变量名一律小写常量用const int加下划线。结构体、类名首字母大写函数名小写开头。所有模板文件都必须有头部注释说明模板用途、适用题目类型、时间/空间复杂度、以及测试例题题号。所有mod操作尽量用long long避免int溢出。规则不一定要多但必须落实到每个文件里。t3code 能长期稳定运行靠的就是这种“规则先行”。3.2 第二步按模块填充模板每个模板都要有“测试用例”写模板不是“写出来就算完事”而是必须配套若干条测试用例。就拿/ds模块来说线段树模板里我随手写了一个水题测试链接但重点是我每次用这个板子过题后都会把这份题的题号记录到文件头部注释里。之所以要记录题号是因为比赛赛场上如果你突然不确定板子的某个细节直接翻这套“验证记录”能快速建立信心。另外每个模块的README.md中应该有一张表记录模板名、适用数据结构/算法、已完成验证的题目、失分的反例。比如模板文件核心内容验证例题注意事项ds/segtree_lazy.cpp懒标记线段树洛谷 P3372更新时先 pushdown 再回溯 pushupgraph/dijkstra.cpp堆优化 Dijkstra洛谷 P4779不适用于负权边math/comb.cpp组合数预处理Codeforces Round #1234模数需为质数这张表刚开始填写时可能觉得麻烦但连续维护 2-3 个月后你会发现它直接变成了你的“比赛指南”遇到新题先看表再选模块最后复制代码改细节。3.3 第三步写一个统一的本地测试脚本把验证变成肌肉记忆t3code 直到第三步才显现出它与其他“模板仓库”的极大不同它不仅是代码库还内置了一套对拍脚本和本地评测工具。平时刷题时我很少直接提交平台习惯先用本地脚本随机生成测试数据与暴力程序对拍确认无误后再提交。这个脚本我是用 Python 写的核心逻辑不到 40 行import os, random, sys for i in range(1000): n random.randint(1, 10) with open(in.txt, w) as f: f.write(f{n}\n) for _ in range(n): f.write(str(random.randint(-10, 10)) ) f.write(\n) os.system(./main in.txt my_out.txt) os.system(./brute in.txt brute_out.txt) if os.system(diff my_out.txt brute_out.txt) ! 0: print(Wrong Answer on test, i) break else: print(All tests passed)这段脚本虽然简单但它做到了一件事让模板的每一次验证都变成自动化的、可重复的流程。以前我改完一个板子都是随手拿个样例跑一遍AC 了就以为没问题。后来加入随机对拍后才发现不少模板在特殊边界下会翻车。多做这一步能挡掉赛场上 90% 的“板子没写熟”问题。3.4 第四步建立竞赛时的“快速检索清单”真正临场使用时仓库再大也是白搭如果你不能在 30 秒内定位到目标代码。因此 t3code 的第四步是建立一份快速检索清单我把它直接写在仓库根目录的README.md里硕大的一行行索引比如区间操作 lazy/ds/segtree_lazy.cpp最短路 负环检测/graph/spfa_negative_cycle.cpp大数组合数取模/math/comb.cpp回文子串统计/strings/manacher.cpp二维偏序 BIT/ds/bit_2d.cpp这份清单不需要详细描述每个板子的用法只需要给出一条“关键词到文件路径”的映射。比赛时我通常先开题确定算法类型再抬头看这份清单30 秒内找到对应板子剩下的时间全花在“把条件转换成代码”的逻辑上。这样做的体感就是从“我要回想模板怎么写”变成了“我知道模板在哪我去抄我自己”。3.5 第五步把每一次比赛都变成 t3code 的迭代机会最后一步也是最容易忽略的一步赛后必须复盘并反向更新仓库。我习惯每场比赛结束后留出 20 分钟拆解题解把那些“我能做但没做出来”的题分类到对应模块然后重新审视自己模板中是否有缺失、冗余或错误。比如我曾在一次周赛里遇到离线处理区间查询的题当时我手头只有传统的线段树和树状数组完全不记得还有“离线排序 BIT”这种组合招数。赛后我立刻在/ds下新增了offline_sort_bit.cpp并且在模板注释里记录了这道原题的具体条件和分析过程。下一个赛季再遇到同类题目我连犹豫都不带犹豫的鼠标、键盘一行行飞起提交一次 AC。这就是 t3code 这个项目里真正的复利效应。4. 踩坑实录与问题排查那些让模板“当场翻车”的细节4.1 边界条件引发的血案数组越界与溢出第一次大规模整理 t3code 时我最得意的就是线段树模板因为它结构非常“标准”看起来无懈可击。结果第一场区域赛练习赛就翻了车我写区间加法树的大小固定开了n 2但输入的n最大是 10 万查询时我调用的是long long的sum与tag可是更新时传入的val用的是int。于是在累加过程中sum[p] val这行代码里val先以int溢出变成了负数然后加到long long上。本地测试可能数据比较小没暴露比赛数据一大就瞬间全错。从那以后我定下死规矩任何与累加和、区间统计有关的变量一律用long long并且在模板头部显式标注“此模板中所有涉及求和数据类型默认为 long long如遇 int 版本需先转换”。现在 t3code 里所有数据结构模板的代码都从编译器警告角度做了一层防护-Wall级别下不允许出现隐式转换杜绝这类边界问题再现。4.2 模板污染复制粘贴时忘了删除调试输出我踩过的第二类坑是模板里混入了调试代码。平时本地跑对拍时为了定位问题经常在代码里插入cout 或者cerr 。但模板文件本身应该是干净的。有一次我因为偷懒把一份带着调试输出的并查集模板直接当成正式代码提交了结果当然是输出一堆乱七八糟的东西白白罚时 20 分钟。t3code 的解决方法是所有模板文件在git commit前都要执行一次“零调试标记检查”我用一个简单的 shell 命令搜索TODO、debug、cout 、cerr等关键词一旦命中就用脚本提示你确认删除。听起来很基础但正是这种强制检查让我在高强度比赛前不用浪费时间反复肉眼核查。4.3 忘记“板子间的依赖关系”而导致编译失败第三个问题更有隐蔽性。我的 t3code 里分模块存了很多代码但有些板子并不是零依赖的比如计算几何里的polygon_area可能依赖/geo/point.cpp里的向量结构体图论里的tarjan_scc可能依赖/core/stack_based_template.cpp里的自定义栈。如果某次我单独复制了一份依赖不全的模板进题解编译失败是必然的。后来我养成了一个习惯每个模板文件的头部注释里不仅要写“适用例题”还要写“前置依赖模块”比如// 依赖: point.cpp (Point 结构体), vector_ops.cpp (叉积与点积) // 测试例题: 洛谷 P2785这样每次复制模板时只需要顺着依赖关系把所有需要的文件一起复制过去。赛前准备的时候我会专门跑一个脚本逐个编译所有模块确认为“独立可编译”这才算一份真正稳定的模板。4.4 对这份板子太自信反而忽略了“读题”本身最后一种坑比较反直觉但特别重要t3code 准备得越充分越容易让人跳过读题和思考环节。有一段时间我一道题看到“区间操作”就直接复制线段树模板结果题里隐含约束是“修改时只改一个点查询时求前缀最小值”用树状数组才是最优解线段树一写多一个 log勉强能过还好常数一大直接 TLE。所以我后来给自己定了一条规则t3code 的价值是“提供通用战斗形态”但不能替代“题感”。如果你发现自己已经把模板背到滚瓜烂熟却开始忽略题目中某些限制条件如数据范围大但操作简单、有多组测试用例、需要离线处理等那么必须强制自己回归到“先思考十秒再打开仓库”的正常节奏。这套代码库再好用也得靠大脑的方向感来驾驭。4.5 模板版本混乱新板和旧板共存造成选择困难很多维护过模板库的人最后都会遇到这个问题一份线段树模板你改了三版每一版都 AC 过题但它们的命名都叫segtree_1.cpp、segtree_2.cpp、segtree_3.cpp。等到赛场上你根本不知道该用哪个版本于是陷入“选择困难”的僵局。t3code 的做法比较粗暴同一算法只保留一个“主版本”其余全部挪进archive/目录。主版本是当前最稳定、最符合代码规范的一版也是唯一直接在比赛中使用的。归档目录里的旧版本不删除只用来做思路回看或性能对比。这样仓库表面看起来很精简但实际深度足够。我实际维护下来感受很深的一点是模板库的“少即是多”远比“面面俱到”更重要。你把 5 种线段树风格全放在首页到了赛场上选板子的开销可能比你现场写代码还大。t3code 的哲学就是每个算法只有一把“主武器”其他的全部锁进仓库深处让低频情况走“现场改造”的流程把高频情况交给“主武器”。5. 实战心得这个项目持续使用半年后的最大改变很多人以为 t3code 带来的最大收益是节省了几分钟的敲代码时间。真不是那只是最表层的收益。它带来的最大改变是让我对“已经会做的题”有了更高的下限。以前遇到熟悉的题型我虽然知道解法但总会在某个小细节上卡一卡要么是边界写错要么是复杂度估错要么是初始值没重置。但有了这套整理和校验过的模板后熟悉题型的 AC 率肉眼可见地暴涨罚时也降了很多。我不需要在基本盘上反复试探省下来的脑力全拿去攻克真正有区分度的题。这个项目的另一个隐性价值是它反过来逼我重新梳理了自己的知识体系。每次往 t3code 里新增一个算法我都要写清楚它的适用条件、与前缀知识的关系、验证题目这一系列动作让我对知识点的理解从“能 AC 这道题”进化到了“能解释为什么这道题 AC”。这种深一层的内化是所有刷题软件都没法直接给你的。如果你也想搞一套自己的 t3code我的建议是不要等知识体系完全成型再动手先把你最常用、最依赖的 5-6 个模板整理出来跑一遍对拍写好注释和依赖关系就已经能感受到显著提升。之后每参加一次比赛每遇见一次“会做但没做干净”的题就更新一次仓库。用不了一个赛季这套库就会变成你的最佳比赛搭档。说到底比赛拼到最后拼的不是谁脑子转得更快而是谁的“下限”更高。t3code 就是在帮你把这个“下限”不断抬高的一种实践方式少一次翻车多一次 AC积少成多就是排名的巨大跃迁。