makedata.h:用最简洁C/C++头文件生成OI测试数据
发布时间:2026/10/1 22:12:11来源:尧图网络
OI圈的日常是什么刷题、补题还有出题。尤其到了校内模拟赛、周赛这种节点总要有人被拉去当出题人。出题最磨人的其实不是想思路也不是写题解而是造数据。数据弱了暴力劈里啪啦 AC数据强了正解反而 WA。我试过很多偷懒的办法最后一直留在手里的是一个单头文件工具makedata.h。这篇就聊聊我为什么还在用这套最简洁的 C/C 测试数据生成工具以及怎么用它快速造出能打的 OI 测试数据。不管你是校赛出题人、训练赛组织者还是单纯想给自己刷题时多造点数据验证正确性甚至只是做数据结构课程设计需要批量“植物百科数据”这种结构化测试内容这套方法都适用。我会把 makedata.h 的接口拆开讲清楚再用一道图论题完整演示“出数据-写暴力-对拍-交付评测”全流程最后把踩过的坑按速查表整理出来希望能让你的出题效率直接翻倍。1. OI出题场景中测试数据是硬骨头1.1 判题数据决定了题目质量做过题的人都知道一道题能不能成为“好题”很大程度上取决于数据强度。数据弱了选手随便写个近似正解的暴力也能过数据太刁钻大家又容易集体翻车。出题人必须照顾多种情况小数据、大数据、链、星、随机图、满二叉树、最坏复杂度构造……这些场景靠手写生成器去一个一个覆盖劳动量非常大而且容易漏。我最早出题时造数据的方式很原始写一个专用生成器再用freopen输出到某个.in文件循环改参数再手动检查格式。遇到图论题还得自己拼边动不动写上百行生成逻辑。最崩溃的是某次模拟赛我写了三个不同的生成脚本输出格式里多打了一个空格评测机上所有选手全部读挂。从那时起我就决定一定要找一套统一、简洁、不容易出错的生成工具。1.2 为什么是“头文件型”工具而不是脚本现在造数据的主流方案其实不止一种。有人用 Python 随机生成再重定向到文件有人用 shell 循环拼接文本还有人干脆手写一堆 C 生成器。这些方案各有问题Python 生成大数据慢shell 拼文本可读性差手写生成器代码复制粘贴严重改一个格式要动所有文件。makedata.h 选择的是“单头文件 C/C 原生”这条路线。它把所有常用随机逻辑、树图生成逻辑、文件输出辅助封装在一个头文件里用的时候#include makedata.h就行。好处非常直接拷贝即用不依赖第三方库不要求判题机装 Python底层和 OI 评测环境一致生成的随机数类型、输出精度都能精准控制编译运行速度快生成百万级点的数据也就几秒钟。对需要频繁出题的场景来说这套方案的性价比非常高。1.3 一个“最简洁”的库应该解决什么问题我这里说的 makedata.h并不是一个躺在网盘里的“官方库”而是我在多次出题过程中反复提炼出来的一个头文件集合。核心功能可以归纳成五块随机基础值生成整数、实数、字符、序列生成排列、去重集合、树生成随机树、链、星、菊花图、图生成连通图、稀疏图、稠密图、带重边自环、文件输出辅助。每个模块尽量精简函数接口控制在三五行以内真正使用时几乎不用看文档靠函数名就能猜出用法。这套设计思路的原则就是生成器代码必须足够短短到出题人愿意每次重新写而不是从旧项目里复制一个改半天。后面我会按这套思路逐一拆解。2. makedata.h 的核心设计与 API 拆解2.1 随机数引擎与种子管理稳定性和可复现同样重要随机数是数据生成的底座。早期很多 OI 选手习惯用srand(time(0)) rand()但这在 Windows 和 Linux 下的行为不一致rand()的周期短、低位随机性差造大量数据时容易出现肉眼可见的规律。makedata.h 里我默认用 C11 的mt19937_64周期够长分布也均匀而且它在各个平台表现一致对出题人来说这是最重要的。种子管理上我保留了“随机种子”和“固定种子”两条路。默认用chrono::steady_clock::now().time_since_epoch().count()取当前时间保证每次生成的数据都不重复同时提供一个set_seed(u64 x)接口方便需要复现某组数据时固定种子。造 10 组数据时我通常会让每组使用不同的种子偏移防止多组之间随机序列高度相似。一个容易被忽略的点uniform_int_distribution的闭区间是[l, r]也就是生成结果包含r。如果手写rnd() % (r - l 1) l要确认上下界设计正确不然边界数据容易差 1这也是对拍时数据造偏的常见原因。2.2 基础生成模块整数、排列、字符串基础模块我习惯提供这些接口long long rnd(long long l, long long r); // 整数 [l, r] double rndf(double l, double r); // 浮点数 [l, r]用于几何/概率题 void shuffle_vector(vectorint v); // 随机打乱 vectorint gen_permutation(int n); // 生成 1..n 的排列 string gen_string(int len, int charset_type); // 随机字符串可限定字符集这里有一个很实用的设计rnd接收long long而不是int。原因很简单OI 题经常出现n 1e9、m 1e18这样的上限如果接口只支持int生成大范围数据时还得自己强转非常影响写生成器的流畅度。gen_permutation我用“初始排序 std::shuffle”实现而不是随机插入这样排列的均匀性更好不会出现某些位置总是偏大的情况。gen_string支持三种字符集小写字母、大小写字母、全字符表达式中常用于模拟题目里的字符串约束。2.3 树与图生成模块随机树不一定适合所有题树是 OI 数据里的主角但不同题对树的形态要求完全不同。链、星、完全二叉树、随机树它们的性质差别极大不能一个生成函数包打天下。makedata.h 里我按形态拆成几个函数vectorpairint,int gen_random_tree(int n); // 随机父节点法 vectorpairint,int gen_chain(int n); // 1-2-3-...-n 链 vectorpairint,int gen_star(int n); // 1 号点连接其余所有点 vectorpairint,int gen_binary_tree(int n); // 尽量均衡地生成二叉树其中gen_random_tree的实现是经典的“随机父亲法”从 2 号点开始每个点随机选择一个编号比它小的点作为父亲。这样生成的树是随机有根树形态比较均匀。但实际出题时很多树上题需要“重链”结构来卡树链剖分或者长链剖分这时纯随机树反而不够用我会把gen_random_tree生成的树打乱编号后再在部分节点之间“拉直”出一条长链。图生成的核心是先保证连通。我采用“先树后边”的方式先用gen_random_tree(n)生成树边再不断添加随机边直到达到目标边数m。这样既能保证连通性又能控制稀疏程度。对于允许重边和自环的情况把随机边改为(rnd(1,n), rnd(1,n))即可不用单独写函数。2.4 文件输出辅助多文件命名和缓冲是隐藏痛点数据生成器最繁琐的部分其实是文件输出。如果每组数据都用freopen循环里面切来切去很容易出错。我的习惯是直接在生成器里用fopen按01.in、02.in的规则命名每次循环关闭文件这样生成器本身就能独立运行不依赖外部重定向。很多 OI 评测系统要求测试点文件必须叫01.in、02.in有的还需要01.out。文件名补零位数要提前约定好如果一共有 20 组数据命名就应该是01.in到20.in而不是1.in到20.in否则排序会乱。我通常用sprintf(name, %02d.in, tc)生成文件名格式统一不容易漏。对于输出量特别大的数据比如几十万行要注意文件流缓冲。直接fpritnf到文件句柄一般没问题但如果用std::ofstream且没关同步可能慢得离谱。出数据时多花点心思在写出效率上后面会省很多等待时间。3. 实操用 makedata.h 从零生成一套完整测试数据3.1 选一道例题求无向图连通块数量为了把流程讲透我用一道非常基础的图论题做演示给定n个点和m条无向边求连通块数量。数据范围设成1 n 2000000 m 400000。题目很常规但正因为常规才能看出数据造得是否全面。如果用手写生成器我需要分别处理最少数据1 个点 0 条边、n3 的小图、随机小图、树形态的连通图、稀疏随机图、稠密图、大数据下的链和大量孤立点。每种边界情况都要单独写一段逻辑代码很快就会膨胀到一两百行。而用 makedata.h整个生成器可以控制在 80 行左右关键形态的区分只靠几个if。3.2 编写最终生成器用“tc 分情况”覆盖所有档位这里给出完整的数据生成器代码编译后一次性生成 10 组.in文件#include makedata.h int main() { set_seed(20250101); // 固定种子方便以后复现同一批数据 for (int tc 1; tc 10; tc) { int n, m; vectorpairint,int edges; if (tc 1) { // 最小数据单点无边 n 1; m 0; } else if (tc 2) { // 小规模手造一个连通与非连通混合样本 n 4; edges {{1, 2}, {2, 3}}; m (int)edges.size(); } else if (tc 3 || tc 4) { // 小规模随机图 n 10; m rnd(5, 15); edges gen_random_graph(n, m, true); } else if (tc 5) { // 一棵随机树n1000mn-1一定连通 n 1000; edges gen_random_tree(n); m (int)edges.size(); } else if (tc 6) { // 稀疏随机图 n 1000; m 2000; edges gen_random_graph(n, m, true); } else if (tc 7) { // 稠密图m 接近 n*(n-1)/2 的比例 n 1000; m 300000; edges gen_random_graph(n, m, true); } else if (tc 8) { // 大数据大量孤立点 n 200000; m 0; } else if (tc 9) { // 大数据一条链 n 200000; edges gen_chain(n); m (int)edges.size(); } else { // 大数据随机图混合结构 n 200000; m 400000; edges gen_random_graph(n, m, true); } char name[32]; sprintf(name, %02d.in, tc); FILE* fout fopen(name, w); fprintf(fout, %d %d\n, n, m); for (auto e : edges) { fprintf(fout, %d %d\n, e.first, e.second); } fclose(fout); printf(case %d: n%d, m%d\n, tc, n, m); } return 0; }这里有一个值得说的细节tc 9的链我用的是gen_chain(n)生成的边是(1,2), (2,3), ...。如果题目要求边的顺序随机可以先把所有边shuffle一遍再输出这样选手程序读入时不会因为“看到边有序”而占到便宜。出题人常犯的一个错误就是生成树后不随机化边的顺序导致某些排序类算法在测试数据上表现异常。3.3 编译运行和生成结果检查在命令行编译时我把生成器命名为gen.cpp执行g -O2 gen.cpp -o gen ./gen输出应该是 10 行提示告诉你每组数据的规模和边数。务必检查每一行的m是否和实际边数一致。最稳妥的方法是写一个“数据校验器”读取每个.in文件统计边数并检查是否连通但简单场景下用文件大小和抽样展示也能判断。我在 Linux 下会顺手跑一句for f in *.in; do echo $f: $(wc -l $f) lines; done确保每个文件的“行数 m 1”。这个习惯帮我拦住过很多次低级错误比如边数少了一条、最后一行没换行之类。Windows 下可以在资源管理器里看文件属性但最好也用一个小脚本检查。3.4 对拍脚本让暴力和正解替你验证数据生成的数据是不是“合法”且“有区分度”得靠对拍来验证。所谓对拍就是用暴力程序保证答案正确再用正解程序跑同一组数据两个结果一致就说明数据的格式和范围没有坑不一致说明某个程序有 bug 或数据违背了题面约束。我习惯把目录按这样组织data/ gen.cpp brute.cpp std.cpp run.sh 01.in 02.in ...暴力程序写法不唯一仍以这题为例用 DFS 求连通块数量的代码如下#include bits/stdc.h using namespace std; const int N 200005; vectorint g[N]; bool vis[N]; void dfs(int u) { vis[u] true; for (int v : g[u]) if (!vis[v]) dfs(v); } int main() { int n, m; scanf(%d%d, n, m); for (int i 0; i m; i) { int u, v; scanf(%d%d, u, v); g[u].push_back(v); g[v].push_back(u); } int ans 0; for (int i 1; i n; i) { if (!vis[i]) { ans; dfs(i); } } printf(%d\n, ans); return 0; }正解std.cpp用并查集实现这里就不再贴全文了。对拍脚本批量跑所有.in把两边输出做 diff#!/bin/bash for f in *.in; do echo testing $f ./brute $f brute.out ./std $f std.out diff brute.out std.out || echo WA on $f doneWindows 下对应的run.bat思路完全一样只是一条条命令手写。对拍全部通过后这批数据才算是“能交付”的。顺带提一个经验对拍时建议把暴力程序和正解程序都编译成-O2否则某些暴力在临界数据上可能超时或者递归栈溢出反而干扰判断。4. 常见问题与排坑实录4.1 随机数据重复或分布不均用rand()时最容易出现这个问题。解决方案上面提过底层换mt19937_64之后基本不再担心重复问题。但即使换了引擎如果你在同一个进程内连续生成 10 组数据且种子都是同一个初始种子下一组数据往往会继承上一组的状态结果就是 10 组数据高度相似。解决办法是每组数据前调用set_seed(some_new_seed)或者在每次循环开始时用当前时间重新初始化。如果遇到需要多组“同分布但不同形态”的数据我会在种子中加入组号比如set_seed(20250101 tc * 1000003)这样每组数据都不同又不会因为时间种子导致生成顺序抖动。4.2 格式化输出错误多空格、少换行、类型不匹配这类问题最常见却最致命。输出边时我把(u, v)写成fprintf(fout, %d %d\n, u, v)如果题目要求的是u和v之间用空格分隔那就不能多打一个空格。有些题目对行末空行不敏感但许多 OI 评测系统要求严谨格式最好养成“用校验程序检查”的习惯。还有一个坑是类型不匹配。题目说n 10^9但生成器里n是int输入输出时用的是%d看起来没问题可如果边权是long long输出时写成%d而后面传的是long long行为就是未定义数据会随机出错而且很难排查。我的原则是生成器里的变量类型必须和题面数据范围一致输出格式符必须和类型一致这个不能靠肉眼要靠编译器告警和校验器双重把关。4.3 边界数据缺失很多新手出题人会把注意力放在随机大数据上结果全是“若干随机图”反而把最容易考到的基础边界漏掉了空图、1 个点、完全图、0 边、只含自环的图、只含两条边的小连通块。这些数据往往能卡掉选手程序里“未初始化数组”“错误认为图必定连通”“根节点编号从 0 开始”等隐蔽 bug。我的策略是在生成器前几个tc分支里强制手写边界后几个分支才交给随机。上面例子中tc 1和tc 8就是在做这件事单点无边、大数据零边。手写边界时不要偷懒按题面逐条对照最小值、最大值、只有一条边、所有点连通、所有边重合等整理成一个检查清单每道题过一遍。4.4 VSCode 下编译 makedata.h 报错“找不到头文件”这个坑在 Windows 上特别常见。你明明把makeadata.h和gen.cpp放在同一个目录但 VSCode 的 C/C 插件不一定认识当前目录编译时甚至根本不进入当前目录导致头文件路径搜索不到。热词里提到的“vscode配置c/c环境”“includePath 优先级”指的基本就是这类问题。解决办法有两层。第一层如果只是临时编译直接命令行进入data目录再g gen.cpp -o gen不要依赖 VSCode 的运行按钮。第二层想用 VSCode 的 F5 或运行任务需要在.vscode/c_cpp_properties.json里配置includePath把当前目录${workspaceFolder}加进去并且在tasks.json里把编译命令的cwd设为${workspaceFolder}或者干脆使用绝对路径包含#include D:/path/to/makedata.h。插件有时候对头文件路径的解析优先级和编译器不同会出现编辑界面上找不到符号、但实际编译能通过的情况这时以编译器的实际行为为准。4.5 浮点数生成与判定精度如果题目涉及浮点数注意rndf(l, r)生成的是double输出时最好用%.10f固定小数位避免科学计数法或多余精度导致选手端读入精度变化。对拍时暴力程序和正解如果都在同一个精度下输出diff 可以过但如果答案要比较大小我一般会写一个带eps的 checker 程序而不是直接 diff 原始输出。出题时数据规模较大浮点误差累积到 1e-4 是常有的事提前规划好误差范围能省去很多调试时间。4.6 常见问题速查表症状可能原因解决办法多组数据完全相同种子固定且未随组号变化每组重置种子或使用time(0) tc边数打印和实际边数不一致生成器逻辑少 push 一条边写校验器统计边数程序读入超时但文件不小输出行数过多且无缓冲使用fprintf或手动加大缓冲VSCode 报头文件找不到includePath 未配置配置c_cpp_properties.json图中连通性无法保证直接用随机边先随机树再加边浮点数误差导致对拍失败输出精度和 eps 未统一固定小数位checker 带 eps5. 出题实测中的一点体会用这套工具出一整套题的效率比我以前手写生成器高太多了。按我的经验熟练之后从拿到题面到交付 10 组数据快则二十分钟大部分时间花在构思边界和检验格式上而不是反复改生成代码。还有一个自己的小习惯我会专门建一个“生成器模板仓库”里面放好 makedata.h 的通用版本以及一个伪随机校验脚本和一个对拍脚本。每次出题只拷贝目录改gen.cpp里的题目逻辑就行。时间久了连调试的套路都固化下来出题这件事就不再是临时抱佛脚而是一个稳定的流程。如果你是第一次用这类工具我不建议一上来就追求复杂的树形结构生成先把整数、排列、随机图这三个模块玩熟配合对拍脚本跑通一套数据后面自然会发现它值得继续扩展。
网站建设高端定制企业官网