简介:基于Python的算法竞赛题目设计源码工具,面向算法竞赛出题人、OJ平台管理员与编程教师,解决题目格式统一难、样例测试繁琐、题目分发不便等痛点,覆盖从题目配置、模板生成到答案校验的完整流程。包内共78个文件,约7.85MB,核心包括15个Python自动化脚本、14个Jinja模板、7个JSON配置及多个Markdown文档,同时提供输入输出样例、答案文件、PNG图示和跨平台格式支持,每一类文件都有明确用途,目录结构便于快速定位。已有325人浏览学习,适合算法竞赛出题、OJ题目建设、编程教学与公开课练习设计等场景。资源不仅包含可直接运行的题目生成与打包工具,还附带竞赛专用LaTeX/HTML模板、Git忽略规则及多平台可执行文件,能帮助用户快速搭建个人题库工作流,也可作为二次开发与教学演示的基础。
1. 基于Python的算法竞赛题目设计源码工具:出题人到底在做什么
一场算法竞赛最磨人的环节,往往不是选手写代码,而是出题人等着数据生成完毕却发现生成的输出文件和题目预期对不上。本地跑一遍样例、交到OJ上却判Wa,最后定位到是题面里一个边界条件没有落到数据生成器里。这个基于Python的算法竞赛题目设计源码工具,核心就是把“出题”从手工造数据、肉眼对答案,变成一套可重复、可版本管理的工程流程。
它解决的问题很具体:批量生成覆盖边界的测试数据、自动构造高压力数据、用校验器统一检查输出格式、把题目三元组(题面、数据、标程)打包成可发布的结构。适合三类人——带学生打比赛需要定期出题的教练、把训练平台内容当核心资产的运营方、以及想在本地用Python快速验证一道题数据强度的个人选手。
工具的本质不是抢出题人的判断力,而是把判断力固化成代码。数据生成一次只能跑出少量样例,真正有价值的是一套能随时重新生成、重新校验、重新打包的源码工程。
2. 数据生成的源码骨架:从随机数到边界数据
2.1 为什么题目设计要先写数据生成器,而不是先写标程
出题人最常犯的顺序错误,是先把标准程序写好、跑几组样例,就开始写题面。这样做的后果是数据强度完全靠运气,题目发布后选手提交各种边界输入,标程却根本没验证过。正确的顺序是:题面定义了输入输出约束,先写数据生成器生成一个覆盖范围的原始数据池,再拿标程去跑数据池,跑不通的地方就是逻辑矛盾和边界漏洞。
生成器的代码不需要复杂,但必须带三个属性:确定性(种子固定后结果可复现)、可调参数(数据规模、值域、极端比例)、可写的文件输出。下面这个例子是经典结构——从纯随机构造向结构化构造过渡。
import random import os def gen_sequence(max_n, max_val, mode="random", seed=20240501): random.seed(seed) if mode == "random": n = random.randint(1, max_n) arr = [random.randint(1, max_val) for _ in range(n)] elif mode == "sorted": n = max_n arr = sorted(random.randint(1, max_val) for _ in range(n)) elif mode == "reverse": n = max_n arr = sorted((random.randint(1, max_val) for _ in range(n)), reverse=True) else: raise ValueError("unknown mode") return n, arr def write_data(filename, n, arr): with open(filename, "w", encoding="utf-8") as f: f.write(f"{n}\n") f.write(" ".join(map(str, arr)) + "\n")逻辑说明:mode参数控制生成模式,random是均匀随机,sorted和reverse分别模拟已排序的输入,这在测二分和单调栈类题目时极其常见。seed不随机分配而作为参数传入,是为了同一道题在不同机器上重新生成数据时结果一致,这是一套源码工具的首要要求。
参数说明:max_n控制规模上限,建议从 10^3 到 10^6 分档;max_val要贴着题面给的值域下界和上界写,避免生成器的值域比题面宽或窄——比题面宽会让数据超过题目约束,比题面窄则覆盖不到上界。
2.2 构造“卡掉错误算法”的数据:造数据的大头不是随机
伪随机数据只能覆盖到概率比较大的路径,真正考验题目质量的是针对性数据。比如一个要求 O(nlogn) 的题目,如果随机数据里恰好有大量重复元素,一个错误的 O(n^2) 冒泡排序也可能蒙混过关。构造卡数据的手段通常有三类:最坏情况模式、密集重复模式、递增递减夹逼模式。
以“给定数组,求最长不下降子序列长度”为例,最坏情况对动态规划解法不敏感,但对暴力法非常敏感。下面这段代码展示了如何在一个生成批次里同时产出“随机小数据”“上升大数据”“完全逆序数据”三种文件。
def gen_all_cases(base_dir, n_small=20, n_big=200000): os.makedirs(base_dir, exist_ok=True) cases = [ ("random_small", lambda: gen_sequence(n_small, 10**9, "random", seed=1)), ("sorted_big", lambda: gen_sequence(n_big, 10**9, "sorted", seed=2)), ("reverse_big", lambda: gen_sequence(n_big, 10**9, "reverse", seed=3)), ] for name, fn in cases: n, arr = fn() write_data(os.path.join(base_dir, f"{name}.in"), n, arr)逻辑说明:gen_all_cases不把文件名写死,而是用name区分不同模式,统一输出.in后缀。数据文件命名不加中文,避免OJ读取时遇到编码问题。seed在每个模式里显式指定,保证重跑只会覆盖同名文件,不会产生“多跑一次多一组数据”的混乱。
一个小建议:生成器尽量写成命令行可调参数的形式,而不是每次改源码。至少留出--seed、--output-dir两个入口,这能让后续批量生成大测试集时不用反复改代码。
3. 标程与校验器:把逻辑绑定成机器可判的规则
3.1 标程不是“能跑的代码”,而是“所有数据对齐的中心”
数据生成器产出输入文件后,标程负责产出对应的输出文件。这里有个常见误会:标程只要在样例上正确就行。实际上,标程是数据合法性的第一道闸门。如果标程在某个生成数据上运行超时,说明数据规模超出题面承诺的时间限制;如果标程报错,说明生成器造出了违反输入约束的数据。
因此标程文件需要和生成器放在同一个工具目录下,由工具统一调度。标准流程是:解析参数 → 循环生成数据 → 调用标程 → 校验输出 → 归档。下面这个调度脚本把整个流程串起来。
import subprocess import sys def run_std_solution(input_file, solver_cmd): with open(input_file, "r", encoding="utf-8") as f_in: data = f_in.read() result = subprocess.run( solver_cmd, input=data, capture_output=True, text=True, timeout=5, ) if result.returncode != 0: raise RuntimeError(f"solver failed on {input_file}: {result.stderr}") return result.stdout def validate_input(input_file): # 校验器只检查格式和范围,不检查逻辑 with open(input_file, "r", encoding="utf-8") as f: first_line = f.readline().strip() n = int(first_line) second_line = f.readline().strip() nums = list(map(int, second_line.split())) assert n == len(nums), f"n={n}, len={len(nums)}" assert all(1 <= x <= 10**9 for x in nums), "value out of range"逻辑说明:run_std_solution用subprocess.run把生成好的输入通过标准输入传给标程,capture_output=True拿到标准输出,timeout参数防止死循环占死整个批次。validate_input是轻量格式校验,它不判断答案对错,只检查数据是否符合题面约束,这是把“数据合法性”与“答案正确性”分离的关键。
这里的取舍是:标程回归测试放在生成之后、正式打包之前,一旦发现输出文件与预期不符,立刻中断整个批次,而不是等全部数据跑完再回头看。中断得越早,浪费的时间越少。
3.2 特殊判题(Special Judge)工具:当答案不是唯一解时
很多题目不只有单一标准答案。比如输出任意合法解、允许浮点误差、或者多解按评分规则给部分分。这个时候标程跑出来的输出文件只能作为参考,不能作为唯一标答。常见做法是把标程的输出交给一个评委程序(special judge)逐项比对,而不是整文件比对。
简易 spj 的 Python 实现可以写成这样:读取选手输出与标准输出,按行切分,对浮点字段用绝对误差或相对误差判断。
def judge_float(num_out, num_std, eps=1e-6): if abs(num_out - num_std) <= eps: return True if abs(num_out - num_std) <= eps * max(abs(num_out), abs(num_std)): return True return False def spj_line_by_line(out_path, std_path, eps=1e-6): with open(out_path, "r", encoding="utf-8") as fo, open(std_path, "r", encoding="utf-8") as fs: lines_out = fo.read().strip().splitlines() lines_std = fs.read().strip().splitlines() if len(lines_out) != len(lines_std): return False for lo, ls in zip(lines_out, lines_std): a_out = list(map(float, lo.split())) a_std = list(map(float, ls.split())) if len(a_out) != len(a_std): return False for x, y in zip(a_out, a_std): if not judge_float(x, y, eps): return False return True逻辑说明:judge_float先做绝对误差判断,再做相对误差判断,两个条件满足一个就算通过。这个策略对齐了大多数OJ对浮点答案的常见容忍度——数据小时看绝对误差,数据大时看相对误差,eps 通常取 1e-6。spj_line_by_line按行比较,同时要求行数和每行元素个数一致,防止选手输出换行位置错乱。
参数说明:这里有个容易忽略的点,整文件strip()之后按splitlines()处理,会忽略文末多余换行,但不会忽略多余空行。出题时最好在 spj 里额外加一道“空行过滤”,否则选手输出多一个空行就会被判Wa,造成体验很差。
4. 避坑:算法竞赛题目设计工具的5个高频坑
4.1 坑一:Windows下生成的数据文件里混入\r\n,评测机读挂
现象:本地生成数据后标程跑得很正常,传到Linux评测机上判题时,读取第一个数字直接报错,或者输出答案全部错误。原因:用open(filename, "w")写入但没指定换行符,Windows默认写成\r\n,而OJ上的读入程序大多只按\n处理,把\r当成数据的一部分。解决:写生成器时统一指定newline="\n",或者在读入端用split()而不是splitlines()来容错。
with open(filename, "w", encoding="utf-8", newline="\n") as f: f.write(...)4.2 坑二:标程和数据生成器吃同一份随机种子,导致数据和答案对不上
现象:数据生成器每次重跑都产生不同数据,而标程跑的是旧数据,归档时输入和输出文件错位。原因:生成器里把random.seed()放在循环外,每一次生成调用都会推进随机状态,但标程结果还没同步更新。解决:在工具入口处固定全局种子,并且把“生成—运行标程—写答案文件”设计成一个不可分割的调用链,一次执行只对应一个数据版本。这条务必写进团队约定里。
4.3 坑三:校验器只查了格式,没查边界值
现象:题面写1 <= a_i <= 10^9,数据生成器却生成了a_i = 0,选手提交绝对值很大的算法也能过,但出题人拿自己标程跑却AC了全部数据。原因:validate_input只检查了数组长度和空行,忘了用题面约束逐个字段判断。解决:把题面的每一段数值约束直接翻译成断言。如果题目值域分段,要按段分开断言。这些断言是源码工具里最容易继承的资产。
4.4 坑四:spj 浮点误差判断写反了,实际允差被放大十倍
现象:部分分判题时,误差在 1e-5 的答案被误判为正确,而在 1e-7 的答案又被判错。原因:abs(num_out - num_std) <= eps * max(abs(num_out), abs(num_std))里的max写成min,导致数值大时相对误差被放大。解决:写完后用一组已知偏移的假输出做单测——分别偏移 1e-3、1e-7、1e-9,确认阈值边界行为符合预期。
assert spj_line_by_line("out_bad.txt", "std.txt", 1e-6) == False assert spj_line_by_line("out_good.txt", "std.txt", 1e-6) == True4.5 坑五:工具目录没有版本管理,改了一版题面忘了改数据
现象:题目改了数据范围(从10^5 改成10^6),生成器参数同步改了,但旧数据文件留在目录里没删,打包时把新旧两套数据混在一起,评测机只识别编号连续的.in/.ans文件,出现空档直接跳过。原因:数据文件和源码工具没有纳入同一套版本管理。解决:每个题目一个独立目录,并配置.gitignore忽略生成的.in/.ans,只提交生成器、标程、校验器和题面源文件。生成物用脚本重新生成,避免手工清理遗漏。
5. 把题目设计源码工具变成团队资产:进入“可复用”阶段
5.1 用配置文件管理题目元数据
工具用久了,最核心的复用不是复制代码,而是复制“出题的规则”。建议在题目目录里放一个problem.yaml这样的元数据文件,把题名、时间限制、内存限制、生成器参数、spj 开关集中管理。生成数据时由脚本读取配置,而不是在生成器代码里硬编码所有参数。这能让一道题的移植成本降低到只改配置。
5.2 批量回归与打包脚本
准备阶段把全部数据跑一遍标程和校验器,用一条命令完成批量回归,同时输出一张摘要表。这张表列清楚每个数据文件的 n、模式、生成时间、标程耗时,方便出题评审时快速判断数据强度边界。打包时再额外生成一份清单文件,写明数据文件与题目约束的对应关系,这样把数据交接给OJ管理员时不需要口头解释。
5.3 从工具到习惯
我把这个工具沉淀下来的最大体会是:出题的速度不会因为写了生成器而变快,但题目质量的下限会被拉得很高。以前一道题容易因为数据不严在赛后被人诟病,现在每次生成数据后都跑一遍针对性构造用例,这种行为本身比工具更重要。希望这些拆解能帮你在下一次出题时少走几步弯路。
本文还有配套的精品资源,点击获取