
简介这是一份面向算法竞赛学习者与ACMer的C代码仓库汇集了多平台题目的AC代码与学习笔记适合正在备战校赛、区域赛或日常刷题提升的选手参考。压缩包共916个文件以577个.cc与289个.cpp源码为主体另有少量Kotlin、Python、Java实现以及Markdown笔记、图片、脚本和构建文件整体约1.03MB体积轻便便于本地检索。内容覆盖动态规划、图论、搜索、数学、数据结构等方向包含ACWing、Contest Hunter、CodeForces等平台的解题代码并附有经典算法模板与参与者的学习笔记文件名统一规范方便按题目或算法快速定位。已有86人学习适合希望借鉴他人解题思路、积累模板与查漏补缺的读者。1. 从一份 C 算法竞赛学习仓库说起它到底能解决什么很多人第一次接触算法竞赛卡住的地方不是不会写代码而是不知道「该按什么顺序练、每道题背后对应哪个数据结构、模板怎么整理才不散」。一份基于 C 的算法竞赛学习仓库本质上就是把这个过程工程化用一套目录结构把常用算法模板、经典例题、刷题笔记和构建脚本收在一起让你 clone 下来就能编译、能跑、能往里面加自己的题解。它适合两类人一是刚学完 C 基础语法、想系统刷题但不知道从哪下手的新手二是打过一段时间比赛、模板散落在十几个 txt 里、想统一管理的熟手。核心词就是 C、算法竞赛、源码、学习仓库——这四个词决定了它不是一个玩具 demo而是一套可以长期维护的个人知识库。下面我按「先立住结构、再动手复现、最后讲坑」的顺序把这类仓库怎么搭、怎么用讲透。2. 仓库目录怎么设计让模板、题解、测试三件事互不打架2.1 为什么不能把所有 .cpp 堆在一个文件夹里新手最常见的做法是建一个code文件夹然后a.cpp、b.cpp、test.cpp一路排下去。刷到第 50 题的时候你已经分不清哪个文件对应哪道题模板改了一处结果三个文件行为不一致。算法竞赛学习仓库的第一个设计目标就是可检索给定一个算法名能立刻定位到模板、例题和笔记。我一般会按「模板 / 题解 / 工具」三条线拆开。模板是稳定的、会被反复复制的代码题解是一次性的、按题目或按专题归档工具是编译脚本、测试数据生成器、对拍脚本这类不直接参与提交的东西。三条线物理隔离改模板不会污染题解跑对拍不会误提交。一个能长期用的目录长这样algorithm-notes/ ├── templates/ # 稳定模板按专题分 │ ├── graph/ │ ├── dp/ │ ├── string/ │ └── math/ ├── solutions/ # 题解按平台或专题分 │ ├── luogu/ │ ├── codeforces/ │ └── nowcoder/ ├── tools/ # 对拍、造数据、编译脚本 ├── notes/ # markdown 笔记 └── CMakeLists.txt关键点是templates和solutions分离。模板文件里只放纯算法实现不写main用#ifndef或者干脆做成头文件题解文件里#include模板自己写输入输出。这样模板改一次所有引用它的题解自动生效。2.2 用 CMake 把整个仓库串起来单个.cpp用g a.cpp -o a编译没问题但仓库里文件一多手动编译就是灾难。常见做法是用 CMake 做统一构建好处是跨平台、能增量编译、能一键跑所有测试。cmake_minimum_required(VERSION 3.15) project(algorithm_notes CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 开优化竞赛代码默认要 -O2 set(CMAKE_CXX_FLAGS_RELEASE -O2 -Wall -Wextra) # 自动收集 solutions 下所有 cpp每个编译成独立可执行文件 file(GLOB_RECURSE SOLUTION_SRCS ${CMAKE_SOURCE_DIR}/solutions/*.cpp) foreach(src ${SOLUTION_SRCS}) # 用相对路径生成目标名避免重名 file(RELATIVE_PATH rel ${CMAKE_SOURCE_DIR}/solutions ${src}) string(REPLACE / _ target_name ${rel}) string(REPLACE .cpp target_name ${target_name}) add_executable(${target_name} ${src}) target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/templates) endforeach()这段脚本做了三件事强制 C17、开-O2和警告、把solutions下每个 cpp 编译成独立可执行文件。target_include_directories把templates加进头文件搜索路径题解里就能直接#include graph/dijkstra.hpp。参数说明CMAKE_CXX_STANDARD 17是竞赛主流标准C20 的concept、ranges在多数 OJ 上还没普及别急着上-Wall -Wextra能提前抓出未使用变量、符号比较这类低级错误比赛时少一次 WA 就赚一次。构建命令mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)-j$(nproc)用满 CPU 核并行编译仓库上百个文件时能省不少时间。2.3 模板文件怎么写才不互相污染模板最容易出的问题是宏定义冲突。比如 A 模板定义了#define int long longB 模板又用了int做循环变量两个一 include 就炸。我的习惯是模板里不写任何全局宏需要long long就老老实实写long long把宏留给题解文件自己控制。以并查集为例模板写成纯头文件// templates/graph/dsu.hpp #pragma once #include vector struct DSU { std::vectorint parent, rank; explicit DSU(int n) : parent(n), rank(n, 0) { for (int i 0; i n; i) parent[i] i; } int find(int x) { // 路径压缩递归写法简洁但深链可能爆栈竞赛里一般够用 return parent[x] x ? x : parent[x] find(parent[x]); } bool unite(int a, int b) { a find(a); b find(b); if (a b) return false; // 按秩合并保证树高 O(log n) if (rank[a] rank[b]) std::swap(a, b); parent[b] a; if (rank[a] rank[b]) rank[a]; return true; } };#pragma once防止重复 include构造函数里初始化parent和rankfind用路径压缩unite用按秩合并两个优化叠加后单次操作近似 O(α(n))。题解里这样用#include graph/dsu.hpp #include cstdio int main() { int n, m; scanf(%d %d, n, m); DSU dsu(n 1); while (m--) { int op, x, y; scanf(%d %d %d, op, x, y); if (op 1) dsu.unite(x, y); else puts(dsu.find(x) dsu.find(y) ? Y : N); } return 0; }模板和题解各司其职模板改 bug 时所有题解一起受益这就是仓库化相对「一堆散文件」的核心价值。3. 把常用算法模板落进仓库从排序到图论的最小可用集3.1 先建哪几个模板别一上来就贪多新手容易犯的错是第一天就想把「算法竞赛所有模板」抄一遍结果抄了 200 个文件一个都没理解。我的建议是先建最小可用集排序、二分、前缀和、并查集、最短路、背包、线段树。这七个覆盖了入门到提高组 80% 的题。剩下的等真正遇到再补补的时候顺手写笔记记忆更牢。以快速排序和归并排序为例竞赛里其实很少手写——std::sort已经足够快。但归并排序的「分治 合并」思想是逆序对、CDQ 分治的基础值得单独留一个模板// templates/sort/merge_sort.hpp #pragma once #include vector // 返回逆序对数量顺便把 arr 排好序 long long merge_sort(std::vectorint arr, int l, int r) { if (r - l 1) return 0; int mid (l r) 1; long long cnt merge_sort(arr, l, mid) merge_sort(arr, mid, r); std::vectorint tmp; tmp.reserve(r - l); int i l, j mid; while (i mid j r) { if (arr[i] arr[j]) tmp.push_back(arr[i]); else { // arr[i] arr[j]说明 arr[i..mid-1] 都大于 arr[j] cnt mid - i; tmp.push_back(arr[j]); } } while (i mid) tmp.push_back(arr[i]); while (j r) tmp.push_back(arr[j]); for (int k 0; k (int)tmp.size(); k) arr[l k] tmp[k]; return cnt; }逻辑说明递归把数组分成两半合并时统计跨越中点的逆序对。当arr[i] arr[j]时左半部分从i到mid-1的所有元素都大于arr[j]一次性加mid - i个。参数l、r是左闭右开区间调用时传0和arr.size()。这个模板同时解决排序和逆序对计数比单独写两个函数更划算。3.2 图论模板Dijkstra 的堆优化写法与边界最短路是图论里出现频率最高的考点。朴素 Dijkstra 是 O(V²)稠密图还行稀疏图必须上堆优化到 O((VE)logV)。仓库里我一般只留堆优化版本因为朴素版几乎用不上。// templates/graph/dijkstra.hpp #pragma once #include vector #include queue #include limits struct Edge { int to, w; }; std::vectorlong long dijkstra(const std::vectorstd::vectorEdge g, int src) { const long long INF std::numeric_limitslong long::max() / 2; int n g.size(); std::vectorlong long dist(n, INF); // 小根堆pair距离, 节点默认按 first 排序 std::priority_queuestd::pairlong long, int, std::vectorstd::pairlong long, int, std::greater pq; dist[src] 0; pq.emplace(0, src); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); // 懒惰删除堆里可能有旧的距离跳过 if (d dist[u]) continue; for (const auto e : g[u]) { if (dist[u] e.w dist[e.to]) { dist[e.to] dist[u] e.w; pq.emplace(dist[e.to], e.to); } } } return dist; }三个关键点INF取LLONG_MAX / 2而不是LLONG_MAX避免加法溢出if (d dist[u]) continue是懒惰删除堆里同一个节点可能有多条记录只处理最新的std::greater让优先队列变成小根堆C17 起可以省略模板参数。注意这个模板不能处理负权边有负权要用 SPFA 或 Bellman-Ford但竞赛里负权图通常有特殊性质别盲目套。3.3 用对拍脚本验证模板正确性模板写完了不代表对。竞赛圈的血泪经验是模板一定要对拍。做法是写一个暴力版本随机生成小数据两个程序跑同一组输入比输出。仓库的tools目录里放这么一套#!/bin/bash # tools/stress_test.sh # 用法: ./stress_test.sh ./build/solution ./build/brute 1000 SOL$1 BRUTE$2 ROUNDS${3:-100} for ((i1; iROUNDS; i)); do python3 tools/gen.py tools/in.txt $SOL tools/in.txt tools/out1.txt $BRUTE tools/in.txt tools/out2.txt if ! diff -q tools/out1.txt tools/out2.txt /dev/null; then echo WA on round $i cat tools/in.txt exit 1 fi done echo All $ROUNDS rounds passedgen.py负责生成符合题目约束的随机数据diff -q静默比较一旦不同就打印输入并退出。参数ROUNDS默认 100调大到 1000 能抓出更隐蔽的边界 bug。这套脚本配合 CMake 编译出的可执行文件就是仓库的自动化测试闭环。4. 避坑与排查算法竞赛仓库最容易翻车的五个地方4.1 现象本地能过提交就 WA原因通常是未定义行为。最常见的是数组越界、int溢出、scanf格式符和变量类型不匹配。本地编译器可能恰好没触发OJ 上换个环境就炸。解决编译时加-fsanitizeaddress,undefined跑一遍样例和随机数据。这个选项会在运行时检查越界和溢出虽然慢但定位问题极准。仓库里可以单独建一个debug构建类型set(CMAKE_CXX_FLAGS_DEBUG -g -O0 -fsanitizeaddress,undefined)4.2 现象模板 include 后编译报重复定义原因模板里写了函数定义而不是声明多个 cpp 同时 include 就违反 ODR单一定义规则。解决模板要么全写成inline要么做成头文件里的struct/class成员函数类内定义默认 inline要么用#pragma once加匿名命名空间。我一般选前两种#pragma once只防同一编译单元重复 include防不了跨编译单元重复定义。4.3 现象对拍跑了几百轮都没问题比赛还是挂原因随机数据太弱。gen.py如果只生成均匀分布的小数据覆盖不到极端情况比如全相同元素、链状图、菊花图。解决针对每个模板写专门的边界生成器。测并查集就生成一条长链测 Dijkstra 就生成一个稠密图和一个稀疏图各跑一遍。数据生成器的质量直接决定对拍的有效性这块偷懒等于没测。4.4 现象CMake 每次全量重编译改一个文件等半天原因file(GLOB_RECURSE)在 CMake 里是配置时求值新增文件不会自动触发重新配置而且很多人没开ccache。解决装ccache并在 CMake 里配置CMAKE_CXX_COMPILER_LAUNCHERccache重复编译同一文件时直接命中缓存。另外新增文件后手动cmake ..重新配置一次别指望 GLOB 自动感知。4.5 现象仓库越用越大git 提交一堆编译产物原因build/目录、可执行文件、测试数据没进.gitignore。解决仓库根目录放一个.gitignorebuild/ *.o *.out tools/in.txt tools/out*.txt只提交源码、模板、笔记和脚本编译产物和临时数据一律忽略。这样仓库 clone 下来干净体积也小。5. 进阶把仓库变成可检索的个人题库仓库搭到一定规模后真正的瓶颈从「怎么写模板」变成「怎么快速找到需要的模板」。我的做法是给每个模板文件加一段结构化注释头然后用脚本生成索引。// templates/graph/dijkstra.hpp // name: Dijkstra 堆优化 // complexity: O((VE)logV) // tags: 图论,最短路,单源 // note: 不支持负权边再写一个 Python 脚本扫描所有模板提取字段生成INDEX.mdimport os, re def build_index(roottemplates): rows [] for dirpath, _, files in os.walk(root): for f in files: if not f.endswith((.hpp, .cpp)): continue path os.path.join(dirpath, f) with open(path, encodingutf-8) as fp: head fp.read(500) name re.search(rname:\s*(.), head) tags re.search(rtags:\s*(.), head) if name: rows.append((name.group(1).strip(), tags.group(1).strip() if tags else , path)) with open(INDEX.md, w, encodingutf-8) as fp: fp.write(| 模板 | 标签 | 路径 |\n|---|---|---|\n) for n, t, p in sorted(rows): fp.write(f| {n} | {t} | {p} |\n) build_index()跑一次生成一张表找模板时直接搜标签。这套东西不复杂但把「翻文件夹」变成了「查索引」长期收益很大。验证方法上我习惯每周挑一个模板从零默写一遍再和仓库里的对比。默写不出来的说明没真正掌握只是抄过。这个习惯帮我抓出过好几个「以为自己会、其实不会」的算法。最后说个我自己的教训早期我追求模板数量抄了三百多个文件结果比赛时一个都想不起来用。后来砍到四十个核心模板每个都手写过、对拍过、默写过反而用得更顺。仓库的价值不在多在于每个文件你都敢在赛场上直接复制。希望帮到你。本文还有配套的精品资源点击获取