COGS Subtask 通信题 配置指南
Subtask 和通信题的配置均利用了 COGS 是串行评测且单次评测目录不变的特性。
本文章部分介绍内容由 AI 代为撰写/润色,所有内容均经过人工核查,如果不对地方请见谅。
捆绑测试(Subtask)配置指南
核心原理
评测系统对每个测试点独立运行一次 SPJ。我们利用 SPJ 可以读写本地文件的特性,用一个临时缓存文件(例如 .subtask_cache)记录每个测试点是否通过。
把每个 Subtask 的最后一个测试点设为“出口点”。运行到出口点时,SPJ 去读缓存,汇总本组所有点的通过状态。如果全对,返回 100(即该组总分配分),否则返回 0。非出口点只根据自身正确性返回 100 或 0,但在 score.cfg 里配置为 0 分,所以不会影响总分。
spj.cpp 的返回值必须是 0 到 100 之间的整数,表示该测试点得分。所有判定由评测系统自动执行。
第一步:数据准备
1. 识别测试点编号(三选一)
SPJ 需要知道当前在评测哪个点。建议根据题目情况选择一种方式:
方式 A:在 .in 文件中显式放置编号
在每个 .in 文件的第一行(或前几个 token)放一个整数,例如 1、2。SPJ 从输入流直接读取这个编号。适用于允许修改输入格式的题目。
方式 B:通过输入内容映射
提取每个 .in 文件前几个固定的整数作为“指纹”,在 SPJ 里用 map 把指纹映射到编号。适用于不想修改输入格式,且前几个整数组合能唯一区分测试点的情况。
方式 C:在 .ans 文件第一行显式放置编号
在每个 .ans 文件第一行写该测试点编号,后面才是标准答案。例如:
SPJ 先读第一行的编号,读指针自然移动到编号之后,checkAnswer 直接从当前位置继续读标准答案即可。这种方式需要修改所有 .ans 文件,且 .ans 第一行不能再作为标准答案内容。
2. 编写 score.cfg
在题目文件夹下建立 score.cfg,按测试点顺序每行一个整数。所有测试点分数总和必须是 100 分。
配置规则:除了每个 Subtask 的最后一个测试点(出口点)外,其余点都设为 0;出口点设为该 Subtask 的总分。
示例:Subtask1 包含点 13(总分 30),Subtask2 包含点 45(总分 70),则 score.cfg 内容为:
第二步:编写 spj.cpp
可以直接套用以下框架,只修改配置区和 checkAnswer 函数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| #include <bits/stdc++.h> using namespace std;
const int ID_MODE = 2;
vector<pair<int,int>> subtasks = {{1,3}, {4,5}};
map<tuple<long long, long long, long long, long long, long long>, int> idMap = { {{300, 200, 100, 6079, 1886}, 1}, };
int getTestId(ifstream& fin, ifstream& fans) { if (ID_MODE == 0) { int id; if (fin >> id) return id; return -1; } else if (ID_MODE == 1) { long long A,B,C,D,E; if (!(fin >> A >> B >> C >> D >> E)) return -1; auto key = make_tuple(A,B,C,D,E); auto it = idMap.find(key); return (it == idMap.end()) ? -1 : it->second; } else if (ID_MODE == 2) { int id; if (fans >> id) return id; return -1; } return -1; }
bool checkAnswer(ifstream& fout, ifstream& fans) { long long outVal, ansVal; if (!(fans >> ansVal)) return false; if (!(fout >> outVal)) return false; long long extra; if (fout >> extra) return false; return outVal == ansVal; }
int getSubtaskLastId(int id) { for (auto &pr : subtasks) if (id >= pr.first && id <= pr.second) return pr.second; return -1; } bool isLastPointOfSubtask(int id) { return getSubtaskLastId(id) == id; }
int main(int argc, char* argv[]) { if (argc != 4) return 0; ifstream fin(argv[1]), fout(argv[2]), fans(argv[3]); if (!fin.is_open() || !fout.is_open() || !fans.is_open()) return 0;
int id = getTestId(fin, fans); fin.close(); if (id == -1) return 0;
bool cur_pass = checkAnswer(fout, fans); fout.close(); fans.close();
int last_id = getSubtaskLastId(id); if (last_id == -1) return 0;
const string CACHE_FILE = ".subtask_cache"; map<int, bool> status; ifstream cache_in(CACHE_FILE); int tid, pass; while (cache_in >> tid >> pass) status[tid] = (pass == 1); cache_in.close();
status[id] = cur_pass;
ofstream cache_out(CACHE_FILE, ios::trunc); for (auto &kv : status) cache_out << kv.first << " " << (kv.second ? 1 : 0) << "\n"; cache_out.close();
if (isLastPointOfSubtask(id)) { int start_id = -1; for (auto &pr : subtasks) if (id >= pr.first && id <= pr.second) { start_id = pr.first; break; } if (start_id == -1) return 0; bool all_pass = true; for (int i = start_id; i <= last_id; ++i) if (!status.count(i) || !status[i]) { all_pass = false; break; } return all_pass ? 100 : 0; }
return cur_pass ? 100 : 0; }
|
通信题评测配置指南
通信题通常有两个角色:发送方 Alice 和接收方 Bob。Alice 根据输入 S 算出一个通信消息 X,Bob 拿到 X 和另一个输入 T 算出最终答案。COGS 不支持多文件编译,也不支持原生双进程交互。这里用基于文件缓存的拆分测试点的办法,在普通 OJ 上模拟通信题评测。
核心原理与前提条件
原理
把一个原测试点拆成两个 OJ 测试点:
- 奇数编号测试点:运行 Alice,计算
X,把 X 写入缓存文件 cache。
- 偶数编号测试点:从
cache 读取上一侧的 X,运行 Bob,输出最终答案。
由于拆分成两个测试点会增加一倍的测试点,为减少评测压力,用 failed 文件做错误传播:只要某个偶数点答案错误,就创建 failed。后续所有测试点的 Grader 一启动就检测到 failed,直接退出;SPJ 也直接返回 0。这样后续测试点全部跳过,节省评测时间。
前提条件
- 每个测试点要有
.in 和 .ans 文件。奇数点的 .ans 可以放占位内容,偶数点的 .ans 必须是正确答案。
- SPJ 返回值是
0 到 100 之间的整数,表示该测试点得分。
第一步:数据准备
1. 拆分测试点
假设原题有 N 个测试点,现在变成 2N 个测试点。编号从 1 开始:
- 奇数编号
1, 3, 5, ... 对应发送方侧。
- 偶数编号
2, 4, 6, ... 对应接收方侧。
每个原测试点 i 拆成 2i-1 和 2i。
2. 准备输入文件 .in
每个 .in 文件第一行格式固定为:
例如:
如果字符串里带空格,读入方式要改成 getline,这里假设字符串无空格。奇数点和偶数点的输入字符串根据题目设计,通常奇数点给 Alice 的 S,偶数点给 Bob 的 T。
3. 准备答案文件 .ans
- 奇数点:不需要标准答案,随便占位。
- 偶数点:放 Bob 应该输出的正确答案。
4. 编写 score.cfg
score.cfg 每行一个整数,共 2N 行,总和必须是 100。
- 奇数行(发送方侧)全部写
0。
- 偶数行(接收方侧)写
100 / N。
例如原题 20 个测试点,则 score.cfg 共 40 行:
一共 20 个 0 和 20 个 5,总和 100。
第二步:编写 Grader 头文件 communicate.h
通信题一般要求选手只提交一个头文件,里面实现 Alice 和 Bob。Grader 也放在这个头文件里。注意:
- 加防重复包含宏。
- 声明选手要实现的函数。
- 内嵌
main 函数作为评测入口。
- 用
namespace Grader 封装辅助工具,避免和选手代码符号冲突。
完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #ifndef COMMUNICATE_H #define COMMUNICATE_H
#include <cstdlib> #include <fstream> #include <iostream> #include <string>
int Alice(std::string S); int Bob(std::string T, int X);
namespace Grader {
const int MAX_X = (1 << 20) - 1;
inline bool hasFailed() { std::ifstream f("failed"); return f.good(); }
inline void markFailed() { std::ofstream f("failed"); f << 1; f.close(); } }
int main() { freopen("communicate.in", "r", stdin); freopen("communicate.out", "w", stdout); if (Grader::hasFailed()) return 0;
int id; std::string str; if (!(std::cin >> id) || !(std::cin >> str)) { Grader::markFailed(); return 0; }
if (id % 2 == 1) { int X = Alice(str);
if (X < 0 || X > Grader::MAX_X) { Grader::markFailed(); return 0; }
std::ofstream cache("cache"); if (!cache) { Grader::markFailed(); return 0; } cache << id << " " << X << std::endl; cache.close();
std::cout << X << std::endl; } else { std::ifstream cache("cache"); if (!cache) { Grader::markFailed(); return 0; } int cachedId, X; if (!(cache >> cachedId >> X) || cachedId != id - 1) { Grader::markFailed(); return 0; } cache.close();
int ans = Bob(str, X); std::cout << ans << std::endl; } return 0; } #endif
|
选手提交的代码大概长这样:
1 2 3 4 5 6 7 8 9 10 11
| #include "communicate.h"
int Alice(std::string S) { return 0; }
int Bob(std::string T, int X) { return 0; }
|
MAX_X 要根据题目实际约束修改。示例里限制 X 在 [0, 1048575](即 (1 << 20) - 1)内,超出范围 Grader 会标记失败。
第三步:编写 spj.cpp
SPJ 作为 OJ 的评测插件,职责是检查 Grader 的输出,并配合 failed 文件实现错误传播。参数固定为 argv[1] 输入文件,argv[2] 选手输出,argv[3] 标准答案。
完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <cstdlib> #include <fstream> #include <iostream> #include <string>
using namespace std;
inline bool hasFailed() { ifstream f("failed"); return f.good(); }
inline void markFailed() { ofstream f("failed"); f << 1; f.close(); }
int main(int argc, char* argv[]) { if (argc < 4) return 0;
if (hasFailed()) return 0;
ifstream fin(argv[1]); int id; if (!(fin >> id)) { markFailed(); return 0; } fin.close();
if (id % 2 == 1) { return 100; }
ifstream fout(argv[2]), fans(argv[3]); int userAns, correctAns; if (!(fout >> userAns) || !(fans >> correctAns)) { markFailed(); return 0; }
string extra; if (fout >> extra) { markFailed(); return 0; }
if (userAns == correctAns) { return 100; } else { markFailed(); return 0; } }
|
如果题目答案不是单个整数,把 userAns == correctAns 那段改成对应判定逻辑即可,比如浮点误差、序列比对、排列合法性等。
评测时序与错误传播样例
以测试点 1 和 2 为例:
测试点 1(奇数)
Grader 调用 Alice,得到 X,检查合法后写入 cache。
SPJ 发现 id 是奇数,返回 100。
因为 score.cfg 第一行是 0,所以实际得分 0。
测试点 2(偶数)
Grader 从 cache 读取上一侧的 X,调用 Bob,输出答案。
SPJ 比对 communicate2.out 和 communicate2.ans。
- 正确:返回 100,
score.cfg 第二行是 5,得 5 分。
- 错误:调用
markFailed(),写入 failed 文件,返回 0,得 0 分。
错误传播
如果测试点 2 写了 failed,那么测试点 3 的 Grader 一启动就会检测到 failed,直接 return 0,不产生任何输出。
测试点 3 的 SPJ 也会检测到 failed,直接返回 0。
后续所有测试点都会这样跳过,直到评测结束。
COGS 虽然很老,但正是这种“老”为其赋予了无穷的拓展性。
对应页面:[COGS]配置子任务 - COGS