简介:这份资源是面向C++学习者与算法竞赛备赛者的CSES Problem Set题解代码合集,针对希望系统提升算法基础与问题解决能力的程序员,覆盖从入门到进阶的完整训练路径。压缩包共28个文件,全部为cpp源码,整体约19KB,按CSES官方分类组织,涵盖基础算法、动态规划、图论与树、贪心、数据结构、数学、回溯剪枝、位运算、模拟建模及编码技巧等模块,如背包、硬币兑换、最小生成树、线段树、并查集、质数判断等经典题目均有对应实现。目前已有256人学习下载,适合准备ACM/ICPC、Google Code Jam等赛事或提升面试表现的读者参考。通过研读这些代码,可对照题目理解状态转移、DFS/BFS、贪心策略与位运算技巧,并借鉴字符串处理、输入输出格式等编码细节,快速建立系统的解题思路与排错方法。
1. 从零拆解 CSES-Problem-Set:一份能直接刷的 C++ 竞赛题单
如果你正在准备算法竞赛,或者想找一套结构清晰、难度递进的题目来系统训练,CSES-Problem-Set 大概率已经被你刷到过。它是一套以 C++ 为主要实现语言的竞赛编程题库,覆盖从入门模拟到高级图论、动态规划的完整知识体系。和零散刷题不同,这套题单按专题分类,每道题都有明确的输入输出约束和测试数据,适合用来做专项突破。我拿到这份资源后第一件事不是急着写代码,而是先看目录结构——它把题目按「Introductory Problems」「Sorting and Searching」「Dynamic Programming」「Graph Algorithms」等模块拆开,每个模块内部难度递增。这意味着你可以按专题逐个击破,而不是随机刷题。对于刚接触竞赛的新手,它能帮你建立完整的知识地图;对于有经验的选手,它是一份高质量的模拟训练集。接下来我会从环境搭建、专题拆解、代码模板、避坑经验几个角度,把这份资源真正用起来。
2. 环境搭建与 C++ 编译参数:把本地跑题流程固定下来
2.1 编译器选择与标准版本
CSES 题目对 C++ 标准没有强制要求,但绝大多数题解和官方测试都基于 C++17 或更高。我一般用 g++ 配合-std=c++17,如果本地是 Windows 环境,MinGW-w64 是常见选择;Linux 下直接装 build-essential 即可。不建议用太老的编译器,因为部分题目会用到结构化绑定、std::optional这类特性,虽然不强制,但写起来更顺手。
先确认版本:
g++ --version如果输出低于 9.0,建议升级。我遇到过用 g++ 7.5 编译时std::gcd找不到的情况,虽然可以手写,但没必要给自己添堵。
2.2 编译参数与调试开关
竞赛代码和工程代码不一样,不需要开一堆警告,但本地调试时我习惯加两个参数:
g++ -std=c++17 -O2 -Wall -Wextra -o solution solution.cpp-O2:开启优化,CSES 部分题目数据量在 1e5 到 2e5 级别,不开优化可能超时。-Wall -Wextra:本地调试时暴露未使用变量、类型转换问题,提交前可以去掉。-o solution:指定输出文件名,避免默认的a.out覆盖。
如果要做性能测试,可以加-pg生成 profiling 信息,但 CSES 本身不要求,按需使用。
2.3 输入输出重定向与本地测试
CSES 的测试数据不公开,但每道题页面会给出样例输入输出。我一般把样例存成input.txt和expected.txt,然后用重定向跑:
./solution < input.txt > output.txt diff output.txt expected.txt这样不用每次手动输入。对于多组样例的题目,可以写个简单的 shell 脚本批量跑:
for i in $(seq 1 5); do ./solution < test$i.txt > out$i.txt diff out$i.txt exp$i.txt && echo "test$i passed" done注意:CSES 的输入格式通常是单组测试,但部分题目有多组查询,读题时要看清。
2.4 目录组织建议
我习惯按专题建目录,每个专题下放题目文件和测试数据:
cses/ ├── introductory/ │ ├── weird_algorithm.cpp │ ├── missing_number.cpp │ └── ... ├── sorting_searching/ │ ├── distinct_numbers.cpp │ └── ... └── templates/ └── fast_io.cpp这样找题和复盘都方便。不要把所有 cpp 文件堆在一个目录里,后期根本分不清哪个是哪个。
3. 专题拆解与代码模板:从 Weird Algorithm 到图论
3.1 入门专题的典型套路
CSES 的 Introductory Problems 部分看起来简单,但有几道题容易翻车。比如 Weird Algorithm 要求模拟 Collatz 序列,看似直接循环就行,但要注意用long long,因为中间值可能超过int范围。我见过有人用int提交后 WA,排查半天才发现是溢出。
再比如 Missing Number,给 n-1 个数,找 1 到 n 中缺失的那个。常见做法是用求和公式,但 n 可能到 2e5,和会超过int,必须用long long。或者用异或,避免溢出:
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); long long n; cin >> n; long long xorsum = 0; for (int i = 0; i < n - 1; i++) { long long x; cin >> x; xorsum ^= x; } for (long long i = 1; i <= n; i++) { xorsum ^= i; } cout << xorsum << "\n"; return 0; }ios::sync_with_stdio(false)和cin.tie(nullptr)是竞赛标配,关闭同步后 cin/cout 速度接近 scanf/printf。- 异或的性质:a ^ a = 0,a ^ 0 = a,所以把所有数和 1 到 n 异或一遍,剩下的就是缺失的数。
- 用
long long是为了防止 n 较大时中间结果溢出,虽然异或本身不溢出,但读入时用 long long 更保险。
3.2 排序与搜索专题的边界条件
Sorting and Searching 部分有几道题对边界很敏感。比如 Distinct Numbers 要求统计不同值的个数,直接排序后去重或者用set都行,但set的常数大,n=2e5 时可能卡时间。我一般用排序:
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); int cnt = 1; for (int i = 1; i < n; i++) { if (a[i] != a[i-1]) cnt++; } cout << cnt << "\n"; return 0; }- 排序后相邻比较,时间复杂度 O(n log n),比
set的 O(n log n) 常数小。 - 注意 n=0 的情况,虽然 CSES 不会给空输入,但写代码时最好习惯性判断。
- 如果题目要求输出具体值而不是个数,可以用
unique去重后输出。
另一道经典题是 Concert Tickets,需要维护一个有序集合并支持删除和查找前驱。用multiset可以,但要注意lower_bound和upper_bound的区别。我一般用multiset加upper_bound找第一个大于目标的位置,然后回退:
auto it = tickets.upper_bound(price); if (it == tickets.begin()) { cout << -1 << "\n"; } else { --it; cout << *it << "\n"; tickets.erase(it); }这里有个坑:erase(it)删除的是迭代器指向的元素,如果先--it再erase,要确保迭代器有效。另外multiset的erase如果传值会删除所有相同元素,传迭代器只删一个。
3.3 动态规划的状态设计
Dynamic Programming 部分是 CSES 的核心难点。以 Dice Combinations 为例,求用 1 到 6 的骰子掷出 n 的方法数。状态转移很直接:dp[i] = sum(dp[i-k]) for k in 1..6,但要注意取模和边界。
#include <bits/stdc++.h> using namespace std; const int MOD = 1e9 + 7; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> dp(n + 1, 0); dp[0] = 1; for (int i = 1; i <= n; i++) { for (int k = 1; k <= 6 && i - k >= 0; k++) { dp[i] = (dp[i] + dp[i - k]) % MOD; } } cout << dp[n] << "\n"; return 0; }dp[0] = 1是初始状态,表示凑出 0 有一种方法(什么都不选)。- 内层循环从 1 到 6,且
i - k >= 0防止越界。 - 取模用
% MOD,注意dp[i] + dp[i-k]可能超过 int,但 MOD 是 1e9+7,两个小于 MOD 的数相加不会超过 2e9+14,在 int 范围内,所以不用 long long。
另一道题 Minimizing Coins 要求用最少的硬币凑出目标金额,状态转移是dp[i] = min(dp[i], dp[i-coin] + 1)。这里要注意初始化dp[0] = 0,其他为无穷大。如果目标金额无法凑出,输出 -1。
3.4 图论专题的存储与遍历
Graph Algorithms 部分涉及 BFS、DFS、最短路、拓扑排序等。CSES 的图通常用邻接表存储,n 和 m 在 1e5 到 2e5 级别,用vector<vector<int>>足够。
以 Counting Rooms 为例,给一个网格图,.表示地板,#表示墙,求房间数。直接 DFS 或 BFS 遍历连通块:
#include <bits/stdc++.h> using namespace std; int n, m; vector<string> grid; vector<vector<bool>> visited; int dx[] = {1, -1, 0, 0}; int dy[] = {0, 0, 1, -1}; void dfs(int x, int y) { visited[x][y] = true; 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 && !visited[nx][ny] && grid[nx][ny] == '.') { dfs(nx, ny); } } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> m; grid.resize(n); visited.assign(n, vector<bool>(m, false)); for (int i = 0; i < n; i++) cin >> grid[i]; int rooms = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (grid[i][j] == '.' && !visited[i][j]) { dfs(i, j); rooms++; } } } cout << rooms << "\n"; return 0; }- 方向数组
dx/dy是网格图遍历的常用技巧,避免写四个 if。 visited用vector<vector<bool>>,注意assign的用法。- 递归 DFS 在 n*m 较大时可能爆栈,CSES 的网格最大 1000x1000,递归深度可能到 1e6,建议改成栈模拟或 BFS。我一般用 BFS 更稳。
4. 避坑与常见问题:那些提交后 WA 或 TLE 的瞬间
4.1 整数溢出导致 WA
现象:本地样例通过,提交后部分测试点 WA。 原因:中间计算结果超过int范围,比如求和、乘积、累加。 解决:看到题目有求和、计数、组合数,优先用long long。CSES 很多题 n 到 2e5,和会到 2e10,必须用 64 位整数。
4.2 递归深度过大导致 RE
现象:提交后运行时错误,本地小数据正常。 原因:DFS 递归深度超过栈限制,CSES 的图或网格可能到 1e5 层。 解决:改成 BFS 或手动栈模拟。如果必须递归,可以加#pragma comment(linker, "/STACK:..."),但跨平台不通用,不如直接改迭代。
4.3 输入输出未关闭同步导致 TLE
现象:算法复杂度正确,但提交后超时。 原因:cin/cout默认与stdio同步,大量输入时慢。 解决:加ios::sync_with_stdio(false); cin.tie(nullptr);。注意加了之后不要混用scanf/printf。
4.4 取模运算顺序错误
现象:结果与预期不符,或者负数。 原因:减法取模时忘记加 MOD,比如(a - b) % MOD可能为负。 解决:写成(a - b + MOD) % MOD,或者用((a - b) % MOD + MOD) % MOD。
4.5 数组越界或未初始化
现象:本地跑出奇怪结果,或者提交后 RE。 原因:vector大小不够,或者全局数组未清零。 解决:用vector时确认resize或assign的大小;全局数组默认清零,但局部数组不会,需要memset或fill。
5. 进阶技巧:用对拍和性能分析把正确率拉满
5.1 对拍脚本的写法
对拍是验证算法正确性的常用手段。写一个暴力解法brute.cpp和一个优化解法fast.cpp,再加一个随机数据生成器gen.cpp,循环比较输出。
// gen.cpp #include <bits/stdc++.h> using namespace std; int main() { srand(time(0)); int n = rand() % 100 + 1; cout << n << "\n"; for (int i = 0; i < n; i++) { cout << rand() % 1000 << " "; } cout << "\n"; return 0; }#!/bin/bash g++ -std=c++17 -O2 -o brute brute.cpp g++ -std=c++17 -O2 -o fast fast.cpp g++ -std=c++17 -O2 -o gen gen.cpp for i in $(seq 1 1000); do ./gen > input.txt ./brute < input.txt > brute_out.txt ./fast < input.txt > fast_out.txt if ! diff -q brute_out.txt fast_out.txt > /dev/null; then echo "Difference found at test $i" cat input.txt break fi done echo "All tests passed"gen.cpp生成随机数据,注意范围要覆盖边界。brute.cpp用最直接的方法,保证正确但可能慢。fast.cpp是你要验证的解法。- 循环 1000 次,发现差异就输出输入并退出。
5.2 性能分析定位瓶颈
如果算法复杂度没问题但 TLE,可以用chrono计时:
auto start = chrono::high_resolution_clock::now(); // 你的代码 auto end = chrono::high_resolution_clock::now(); cerr << "Time: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << " ms\n";或者用gprof:
g++ -std=c++17 -O2 -pg -o solution solution.cpp ./solution < input.txt gprof solution gmon.out > analysis.txt看analysis.txt里哪个函数占用时间最多,针对性优化。
5.3 常见优化手段
- 用
vector代替list,缓存友好。 - 用数组代替
unordered_map,如果键范围小。 - 减少函数调用,把短函数写成宏或内联。
- 用
scanf/printf如果cin/cout优化后仍慢。 - 避免频繁
push_back不预留空间,用reserve。
5.4 复盘习惯
我每次做完一道题,不管 AC 还是 WA,都会把代码和思路记下来。WA 的题标注错误原因,AC 的题看有没有更优解法。CSES 的题目质量很高,值得反复刷。从那以后我每次提交前都强制走一遍对拍和边界测试,虽然多花几分钟,但省下了大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取