简介:这是一份面向算法竞赛学习者与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()这段脚本做了三件事:强制 C++17、开-O2和警告、把solutions下每个 cpp 编译成独立可执行文件。target_include_directories把templates加进头文件搜索路径,题解里就能直接#include "graph/dijkstra.hpp"。
参数说明:CMAKE_CXX_STANDARD 17是竞赛主流标准,C++20 的concept、ranges在多数 OJ 上还没普及,别急着上;-Wall -Wextra能提前抓出未使用变量、符号比较这类低级错误,比赛时少一次 WA 就赚一次。构建命令:
mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc)-j$(nproc)用满 CPU 核并行编译,仓库上百个文件时能省不少时间。
2.3 模板文件怎么写才不互相污染
模板最容易出的问题是宏定义冲突。比如 A 模板定义了#define int long long,B 模板又用了int做循环变量,两个一 include 就炸。我的习惯是模板里不写任何全局宏,需要long long就老老实实写long long,把宏留给题解文件自己控制。
以并查集为例,模板写成纯头文件:
// templates/graph/dsu.hpp #pragma once #include <vector> struct DSU { std::vector<int> 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和rank;find用路径压缩,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::vector<int>& 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::vector<int> 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((V+E)logV)。仓库里我一般只留堆优化版本,因为朴素版几乎用不上。
// templates/graph/dijkstra.hpp #pragma once #include <vector> #include <queue> #include <limits> struct Edge { int to, w; }; std::vector<long long> dijkstra(const std::vector<std::vector<Edge>>& g, int src) { const long long INF = std::numeric_limits<long long>::max() / 2; int n = g.size(); std::vector<long long> dist(n, INF); // 小根堆:pair<距离, 节点>,默认按 first 排序 std::priority_queue<std::pair<long long, int>, std::vector<std::pair<long 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<>让优先队列变成小根堆,C++17 起可以省略模板参数。注意这个模板不能处理负权边,有负权要用 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 ((i=1; i<=ROUNDS; 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 passed"gen.py负责生成符合题目约束的随机数据,diff -q静默比较,一旦不同就打印输入并退出。参数ROUNDS默认 100,调大到 1000 能抓出更隐蔽的边界 bug。这套脚本配合 CMake 编译出的可执行文件,就是仓库的自动化测试闭环。
4. 避坑与排查:算法竞赛仓库最容易翻车的五个地方
4.1 现象:本地能过,提交就 WA
原因通常是未定义行为。最常见的是数组越界、int溢出、scanf格式符和变量类型不匹配。本地编译器可能恰好没触发,OJ 上换个环境就炸。
解决:编译时加-fsanitize=address,undefined,跑一遍样例和随机数据。这个选项会在运行时检查越界和溢出,虽然慢,但定位问题极准。仓库里可以单独建一个debug构建类型:
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fsanitize=address,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_LAUNCHER=ccache,重复编译同一文件时直接命中缓存。另外新增文件后手动cmake ..重新配置一次,别指望 GLOB 自动感知。
4.5 现象:仓库越用越大,git 提交一堆编译产物
原因:build/目录、可执行文件、测试数据没进.gitignore。
解决:仓库根目录放一个.gitignore:
build/ *.o *.out tools/in.txt tools/out*.txt只提交源码、模板、笔记和脚本,编译产物和临时数据一律忽略。这样仓库 clone 下来干净,体积也小。
5. 进阶:把仓库变成可检索的个人题库
仓库搭到一定规模后,真正的瓶颈从「怎么写模板」变成「怎么快速找到需要的模板」。我的做法是给每个模板文件加一段结构化注释头,然后用脚本生成索引。
// templates/graph/dijkstra.hpp // @name: Dijkstra 堆优化 // @complexity: O((V+E)logV) // @tags: 图论,最短路,单源 // @note: 不支持负权边再写一个 Python 脚本扫描所有模板,提取@字段生成INDEX.md:
import os, re def build_index(root="templates"): 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, encoding="utf-8") as fp: head = fp.read(500) name = re.search(r"@name:\s*(.+)", head) tags = re.search(r"@tags:\s*(.+)", head) if name: rows.append((name.group(1).strip(), tags.group(1).strip() if tags else "", path)) with open("INDEX.md", "w", encoding="utf-8") as fp: fp.write("| 模板 | 标签 | 路径 |\n|---|---|---|\n") for n, t, p in sorted(rows): fp.write(f"| {n} | {t} | `{p}` |\n") build_index()跑一次生成一张表,找模板时直接搜标签。这套东西不复杂,但把「翻文件夹」变成了「查索引」,长期收益很大。
验证方法上,我习惯每周挑一个模板,从零默写一遍再和仓库里的对比。默写不出来的说明没真正掌握,只是抄过。这个习惯帮我抓出过好几个「以为自己会、其实不会」的算法。
最后说个我自己的教训:早期我追求模板数量,抄了三百多个文件,结果比赛时一个都想不起来用。后来砍到四十个核心模板,每个都手写过、对拍过、默写过,反而用得更顺。仓库的价值不在多,在于每个文件你都敢在赛场上直接复制。希望帮到你。
本文还有配套的精品资源,点击获取