简介:CSES Problem Set 是面向C++算法学习者与编程竞赛选手的高质量题解资源,覆盖基础算法、动态规划、图论、贪心、数据结构、数论、回溯、位运算及模拟建模等10大核心模块,精准匹配ACM/ICPC、Code Jam及大厂技术面试对算法能力的系统性要求。资源包为ZIP格式,共28个独立可运行的C++源文件(.cpp),涵盖字符串匹配、树距离、最小生成树、线段树、质数判断、棋盘回溯等典型题目实现,总大小仅19KB,轻量便携,代码规范清晰,便于逐题调试与原理验证。已有258人下载学习,适合从入门到进阶的算法实践者——不仅提供完整可编译的参考实现,更通过题目归类与模块化组织(如String Algorithms、Graph Algorithms、Mathematics等子目录)体现清晰的学习路径,助力读者建立结构化算法知识体系与扎实编码习惯。
1. 这不是刷题网站的镜像,而是一套可本地运行、可断点调试、可二次封装的 C++ 算法验证沙盒
你有没有试过:在某个在线判题平台 AC 一道动态规划题,但把代码复制到本地 g++ 编译后,std::vector越界不报错、long long溢出没警告、输入格式稍有偏差就卡死——最后发现是平台隐藏了-D_GLIBCXX_DEBUG,还替你加了ios_base::sync_with_stdio(false)?CSES-Problem-Set 就是反其道而行之:它不提供网页界面,不托管测试数据,不封装 IO;它只给你一份纯 C++ 源码集合 + 标准化输入输出协议 + 可复现的本地验证脚本。所有题目以.cpp文件形式组织,每个文件自带main(),读标准输入、写标准输出,完全符合 ISO/IEC 14882 标准。它适合三类人:刚学完 STL 想验证std::set查找复杂度的新人、准备算法面试需反复调试状态转移的中阶开发者、以及需要把经典题解嵌入自己工程(比如作为校验模块)的系统工程师。这不是题库,是算法的「最小可执行单元」。
2. 项目结构与核心约定:从目录树读懂它的设计哲学
CSES-Problem-Set 的本质是一个高度自律的 C++ 工程实践样本。它没有CMakeLists.txt,没有build/目录,甚至没有src/分层——所有.cpp文件平铺在根目录下,按分类子目录组织(如sorting_and_searching/,graph_algorithms/)。这种“扁平即正义”的结构背后,藏着三个硬性约定,它们共同构成了整个项目的可维护性基石。
2.1 文件命名与分类逻辑:用路径表达算法语义
项目采用两级路径分类:
/ ├── sorting_and_searching/ │ ├── distinct_numbers.cpp │ ├── apartments.cpp │ └── ... ├── dynamic_programming/ │ ├── coin_combinations_i.cpp │ ├── removing_digits.cpp │ └── ... ├── graph_algorithms/ │ ├── counting_rooms.cpp │ ├── shortest_route.cpp │ └── ... └── ...每个子目录名本身就是一类算法范式的标签。distinct_numbers.cpp不叫p1234.cpp,因为它的价值不在题号,而在“去重”这个操作本身。更关键的是:所有文件名小写+下划线,且与题目英文描述关键词严格对齐。这意味着你可以用grep -r "coin.*combination" .快速定位动态规划中组合类问题,而不是靠记忆题号翻页。这种命名不是风格偏好,而是为 CLI 工作流服务的基础设施——后续你会看到,所有自动化验证脚本都依赖这个约定。
2.2 单文件自包含:无外部依赖,无宏污染,无全局状态
打开任意一个.cpp文件(例如dynamic_programming/coin_combinations_i.cpp),你会发现它满足以下四条铁律:
- 头文件极简:仅含
<iostream>,<vector>,<algorithm>,<climits>等标准库,绝无<bits/stdc++.h>或自定义头文件; - 无
#define int long long类型劫持:所有整数类型显式声明(int,long long,unsigned),避免隐式转换陷阱; - 无全局变量:所有数据结构均在
main()内声明或通过函数参数传递; - IO 严格遵循标准流:
std::cin >> n;读整数,std::cout << ans << '\n';输出答案,不用scanf/printf,也不用endl(避免 flush 开销)。
这种约束看似严苛,实则是为了确保:
✅ 任何文件双击即可用g++ -std=c++17 -O2 file.cpp -o file && ./file编译运行;
✅ 所有文件可被#include到更大工程中(只要保证命名空间干净);
✅ 静态分析工具(如clang-tidy)能直接扫描,无需配置排除规则。
提示:如果你习惯用
using namespace std;,请暂时戒掉。CSES 所有文件都显式写std::vector,这是为防止std::distance与自定义distance()函数发生 ADL 冲突——某次我在模拟项目X中因漏掉这点,导致std::lower_bound在自定义迭代器上静默调用错误重载,调试 3 小时才发现。
2.3 输入输出协议:统一格式即统一契约
CSES 对每道题的输入输出格式做了机器可读的标准化。以graph_algorithms/counting_rooms.cpp为例:
// Input: first line has h, w (height and width) // next h lines each have w characters ('.' for floor, '#' for wall) // Output: single integer — number of connected components of '.'注意两点:
- 注释中明确写出行数、分隔符、字符含义、边界条件(如
h, w ≤ 1000在 README.md 中统一声明); - 输出永远是单个值或单行字符串,绝不出现
"Answer: " + to_string(ans)这类干扰判题的前缀。
这个协议让自动化验证成为可能。后续章节会展示如何用 Python 脚本生成千组随机测试用例,并用diff校验输出——这正是它区别于“网页题库”的核心能力:把算法正确性验证从黑匣子交互,变成白盒可追踪的编译-运行-比对闭环。
3. 本地运行全流程:从零开始跑通第一道题并验证结果
别急着写代码,先让环境“认得”这个项目。CSES 不要求 Docker 或虚拟环境,但它对编译器版本和标准库行为有明确要求。下面是以 Ubuntu 22.04 / macOS Ventura 为基准的完整链路,覆盖新手最易卡壳的五个环节。
3.1 环境准备:确认你的 g++ 支持 C++17 且无 ABI 兼容问题
CSES 所有代码使用 C++17 特性(如std::optional,if constexpr, 结构化绑定)。执行以下命令验证:
g++ --version # 应输出类似:g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 # 若版本 < 9.0,请升级:sudo apt update && sudo apt install g++-11关键检查项不是版本号,而是标准库 ABI 兼容性。某些旧版 libstdc++ 在std::string_view构造时存在内存越界(已知影响sorting_and_searching/apartments.cpp)。快速验证:
echo '#include <string_view>' | g++ -x c++ -std=c++17 -c -o /dev/null - # 若报错 "error: 'string_view' is not a member of 'std'",说明标准库太旧注意:macOS 用户若用 Homebrew 安装的 gcc,务必用
g++-11而非系统clang++,因为后者默认不启用 GNU 扩展的__gnu_cxx::hash_map(虽 CSES 不用,但避免后续扩展踩坑)。
3.2 编译单个文件:理解-O2和-D_GLIBCXX_DEBUG的取舍
以sorting_and_searching/distinct_numbers.cpp为例:
# 生产级编译(推荐首次运行) g++ -std=c++17 -O2 distinct_numbers.cpp -o distinct_numbers # 调试编译(发现越界时启用) g++ -std=c++17 -D_GLIBCXX_DEBUG -g distinct_numbers.cpp -o distinct_numbers_debug参数详解:
-O2:开启二级优化,保留调试信息(-g可选),平衡速度与可调试性;-D_GLIBCXX_DEBUG:激活 GNU libstdc++ 的容器调试模式,使vector[i]越界立即 abort,而非静默 UB;- 切勿同时用
-O2和-D_GLIBCXX_DEBUG:前者会内联函数,后者需函数符号,冲突导致编译失败。
运行验证:
# 准备输入(按题目要求:第一行n,第二行n个整数) echo -e "5\n2 3 2 2 3" | ./distinct_numbers # 输出应为:2 (表示不同数字个数)3.3 批量验证:用 Python 脚本驱动测试用例
CSES 官方不提供测试数据,但项目根目录下有testgen/子目录(需手动创建),里面存放生成器脚本。我们用一个通用生成器gen_random_ints.py:
# testgen/gen_random_ints.py import sys import random def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 1000 # 生成 n 个 [1, 1000] 范围内的随机整数 nums = [random.randint(1, 1000) for _ in range(n)] print(n) print(" ".join(map(str, nums))) if __name__ == "__main__": main()然后编写验证脚本verify.py:
#!/usr/bin/env python3 import subprocess import sys import os def run_solution(sol_path, input_data): """运行可执行文件,返回 stdout 字符串""" result = subprocess.run( [sol_path], input=input_data, text=True, capture_output=True, timeout=5 ) return result.stdout.strip() def count_distinct(nums): """Python 参考实现""" return len(set(nums)) if __name__ == "__main__": sol = "./distinct_numbers" # 生成 10 组测试 for i in range(10): # 生成输入 input_data = subprocess.check_output( ["python3", "testgen/gen_random_ints.py", "100"] ).decode().strip() # 获取 C++ 输出 cpp_out = run_solution(sol, input_data) # 解析输入得到期望答案 lines = input_data.strip().split('\n') n = int(lines[0]) nums = list(map(int, lines[1].split())) expected = str(count_distinct(nums)) if cpp_out != expected: print(f"❌ Test {i} failed!") print(f"Input: {input_data}") print(f"Expected: {expected}, Got: {cpp_out}") sys.exit(1) print("✅ All tests passed!")执行:python3 verify.py。这个脚本的价值在于:它把“人工肉眼比对”变成了可重复、可集成到 CI 的步骤。当你修改算法时,只需改一行cpp_out = ...就能切换对比基准(比如换成 Rust 实现)。
3.4 常见问题排查:五条血泪经验总结
现象 → 原因 → 解决,全是真实翻车现场:
现象:
./distinct_numbers运行后卡住,Ctrl+C 显示SIGINT,但无输出
原因:输入数据末尾缺换行符,std::cin >> n阻塞等待;CSES 要求输入严格按行,最后一行必须有\n
解决:用echo -e "5\n2 3 2 2 3\n" | ./distinct_numbers(注意末尾\n),或在生成器中print()后加空行现象:
g++报错error: 'optional' is not a member of 'std'
原因:编译器版本 ≥ 10 但未启用 C++17 标准,或头文件未包含<optional>(CSES 文件已包含,故必是编译参数问题)
解决:确认命令为g++ -std=c++17 ...,而非-std=gnu++17(后者可能禁用部分标准特性)现象:本地输出
2,但提交到在线平台显示 WA(Wrong Answer)
原因:平台输入含 Windows 换行符\r\n,而std::cin默认跳过空白,但某些情况下\r被当作字符读入
解决:在main()开头加std::cin.sync_with_stdio(false); std::cin.tie(nullptr);并确保输入生成器用 Unix 换行(echo -e默认满足)现象:
make命令报错make: *** No targets. Stop.
原因:CSES 无 Makefile,新手误以为需 make;项目设计就是g++直接编译
解决:删掉所有Makefile相关尝试,回归g++ -std=c++17 xxx.cpp -o xxx现象:
valgrind ./distinct_numbers显示Invalid read of size 8
原因:代码中vector<int> a(n); a[n] = 1;越界,但-O2优化后未触发崩溃
解决:先用-D_GLIBCXX_DEBUG编译复现,定位越界位置;C++17 下可用a.at(n)替代a[n]强制边界检查
4. 核心模块拆解:dynamic_programming/coin_combinations_i.cpp的完整链路
这道题是 CSES 动态规划入门题,求用给定面额硬币组成目标金额的方案数(每种硬币无限使用)。它短小精悍(仅 25 行),却浓缩了背包问题的精髓。我们逐段解析,重点看它如何用最简 C++ 表达状态转移,并规避常见陷阱。
4.1 完整源码与逐行注释
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int n, x; cin >> n >> x; // n: coin types, x: target sum vector<int> coins(n); for (int i = 0; i < n; i++) { cin >> coins[i]; } // dp[i] = number of ways to make sum i // Use long long to avoid overflow on large x (x <= 1e6) vector<long long> dp(x + 1, 0); dp[0] = 1; // base case: one way to make sum 0 (use no coins) // Iterate over coins first -> ensures unlimited supply per coin for (int coin : coins) { for (int s = coin; s <= x; s++) { dp[s] += dp[s - coin]; } } cout << dp[x] << '\n'; }关键点解析:
ios_base::sync_with_stdio(false); cin.tie(nullptr);:解除 C 与 C++ IO 流同步,提升大输入读取速度(x 可达 1e6,输入行数多);vector<long long> dp(x + 1, 0):x最大为 1e6,dp数组大小 1e6+1,用long long防止方案数溢出(如全用 1 元硬币,方案数 = 1);- 外层遍历 coins,内层遍历 s 从 coin 到 x:这是“完全背包”的标准写法,保证每种硬币可重复使用;若内外层颠倒,则变成“0-1 背包”(每种硬币只能用一次);
dp[0] = 1:数学上,组成 0 元只有一种方法——不选任何硬币;这是所有 DP 的起点,漏掉则全盘皆错。
4.2 状态转移的物理意义:为什么dp[s] += dp[s - coin]?
假设当前处理硬币面额coin = 2,目标s = 5:
dp[5]表示组成 5 元的所有方案数;dp[5-2] = dp[3]表示组成 3 元的所有方案数;- 每一种组成 3 元的方案,末尾加一枚 2 元硬币,就得到一种组成 5 元的方案;
- 因此,新增方案数 =
dp[3],累加到dp[5]。
这个逻辑成立的前提是:dp[3]已包含所有用之前硬币(如 1 元)组成的方案。所以必须先固定 coin,再从小到大更新 s——这就是循环顺序的设计依据。
4.3 边界条件与数值安全:x = 0和溢出防护
当x = 0时:
vector<long long> dp(1, 0)创建大小为 1 的数组;dp[0] = 1正确设置;- 外层
for (int coin : coins)中,内层for (int s = coin; s <= x; s++)因coin >= 1且x = 0,循环体不执行; - 最终输出
dp[0] = 1,符合数学定义。
溢出防护体现在两处:
long long类型:x ≤ 1e6,最坏情况(全 1 元)方案数为 1,不会溢出;但若硬币含 1 元,方案数实际是组合数,long long可支撑x ≤ 1e6;dp数组大小x + 1:若x = 0,x + 1 = 1,无越界;若x = 1e6,dp占用约 8MB 内存,在现代机器上安全。
提示:我曾在模拟项目X中把
dp声明为vector<int>,测试时x = 100000时dp[100000]溢出为负数,导致后续计算全错。从此养成习惯:凡计数类 DP,无脑long long,除非明确知道上界小于2^31。
4.4 可视化调试技巧:用cerr打印中间状态
当逻辑复杂时,在main()中插入调试输出(发布前删除):
// 在双重循环内添加(仅调试用) // for (int coin : coins) { // cerr << "Processing coin: " << coin << "\n"; // for (int s = coin; s <= x; s++) { // long long prev = dp[s]; // dp[s] += dp[s - coin]; // cerr << "dp[" << s << "] = " << prev << " + dp[" << (s-coin) << "] = " << dp[s] << "\n"; // } // }cerr输出到 stderr,不影响 stdout 的答案输出,且g++ -O2不会优化掉cerr(与cout不同)。这是比 IDE 断点更轻量的调试方式,尤其适合算法题。
5. 二次开发实战:把 CSES 题解封装成 C++ 库函数供其他项目调用
这才是 CSES 的隐藏价值——它不是终点,而是你工程中的一个模块。设想你在开发某跨平台系统,需要频繁计算“硬币组合数”,但不想每次复制粘贴coin_combinations_i.cpp。下面教你把它变成可链接的静态库,并支持 C 风格调用。
5.1 提取核心逻辑为独立头文件
新建lib/coin_combinations.h:
#ifndef COIN_COMBINATIONS_H #define COIN_COMBINATIONS_H #ifdef __cplusplus extern "C" { #endif /** * @brief Calculate number of ways to make target sum with given coins * @param coins: array of coin denominations * @param n: number of coin types * @param x: target sum (0 <= x <= 1000000) * @return number of combinations, or -1 on error (e.g., x < 0) */ long long coin_combinations(const int* coins, int n, int x); #ifdef __cplusplus } #endif #endif新建lib/coin_combinations.cpp:
#include "coin_combinations.h" #include <vector> #include <algorithm> long long coin_combinations(const int* coins, int n, int x) { if (x < 0) return -1; if (x == 0) return 1; std::vector<long long> dp(x + 1, 0); dp[0] = 1; for (int i = 0; i < n; i++) { int coin = coins[i]; if (coin <= 0) continue; // skip invalid coins for (int s = coin; s <= x; s++) { dp[s] += dp[s - coin]; } } return dp[x]; }关键改造点:
- 添加
extern "C"块:确保 C++ 编译器生成 C 风格符号,避免 name mangling; - 参数改为指针+长度:符合 C 接口惯例,调用方无需
std::vector; - 增加输入校验:
x < 0返回 -1,coin <= 0跳过,增强鲁棒性; - 移除所有
using namespace std和ios_base设置:库函数不应干涉调用方 IO 状态。
5.2 编译为静态库并链接使用
# 编译为对象文件 g++ -std=c++17 -c lib/coin_combinations.cpp -o lib/coin_combinations.o # 打包为静态库 ar rcs libcoin.a lib/coin_combinations.o # 使用示例:test_user.cpp #include <iostream> #include "lib/coin_combinations.h" int main() { int coins[] = {1, 2, 5}; long long res = coin_combinations(coins, 3, 10); std::cout << "Ways: " << res << "\n"; // 输出 10 }编译链接:
g++ -std=c++17 test_user.cpp -L. -lcoin -o test_user ./test_user # 输出 Ways: 105.3 进阶:支持自定义分配器与异常安全
若你的系统对内存敏感(如嵌入式),可进一步改造coin_combinations接受自定义分配器:
// 在头文件中增加重载 long long coin_combinations_custom_alloc( const int* coins, int n, int x, void* (*alloc_func)(size_t), // malloc-like void (*free_func)(void*) // free-like );实现中用alloc_func(sizeof(long long) * (x+1))替代std::vector,并在函数退出前调用free_func。这需要你熟悉placement new,但 CSES 的简洁结构让这种改造成本极低——因为原始代码无全局状态、无资源泄漏点。
从那以后我每次把算法题解迁移到生产环境,都强制走一遍“提取头文件 → 编译静态库 → 编写 C 调用示例 → valgrind 检查内存”的流程。哪怕只是临时脚本,这套动作也成了肌肉记忆。它逼我思考:这段代码的输入契约是什么?输出是否可预测?失败时能否优雅降级?CSES 不教这些,但它用最朴素的.cpp文件,给了我练习这些工程直觉的沙盒。希望帮到你。
本文还有配套的精品资源,点击获取