Subtask 和通信题的配置均利用了 COGS 是串行评测且单次评测目录不变的特性。
本文章部分介绍内容由 AI 代为撰写/润色,所有内容均经过人工核查,如果不对地方请见谅。
评测系统对每个测试点独立运行一次 SPJ。我们利用 SPJ 可以读写本地文件的特性,用一个临时缓存文件(例如 .subtask_cache)记录每个测试点是否通过。
把每个 Subtask 的最后一个测试点设为“出口点”。运行到出口点时,SPJ 去读缓存,汇总本组所有点的通过状态。如果全对,返回 100(即该组总分配分),否则返回
0。非出口点只根据自身正确性返回 100 或 0,但在 score.cfg 里配置为 0 分,所以不会影响总分。
spj.cpp 的返回值必须是 0 到 100 之间的整数,表示该测试点得分。所有判定由评测系统自动执行。
SPJ 需要知道当前在评测哪个点。建议根据题目情况选择一种方式:
方式 A:在 .in 文件中显式放置编号
在每个 .in 文件的第一行(或前几个 token)放一个整数,例如 1、2。SPJ 从输入流直接读取这个编号。适用于允许修改输入格式的题目。
方式 B:通过输入内容映射
提取每个 .in 文件前几个固定的整数作为“指纹”,在 SPJ 里用 map 把指纹映射到编号。适用于不想修改输入格式,且前几个整数组合能唯一区分测试点的情况。
方式 C:在 .ans 文件第一行显式放置编号
在每个 .ans 文件第一行写该测试点编号,后面才是标准答案。例如:
1 42
SPJ 先读第一行的编号,读指针自然移动到编号之后,checkAnswer 直接从当前位置继续读标准答案即可。这种方式需要修改所有 .ans 文件,且 .ans 第一行不能再作为标准答案内容。
score.cfg
在题目文件夹下建立 score.cfg,按测试点顺序每行一个整数。所有测试点分数总和必须是 100 分。
配置规则:除了每个 Subtask 的最后一个测试点(出口点)外,其余点都设为 0;出口点设为该 Subtask 的总分。
示例:Subtask1 包含点 1~3(总分 30),Subtask2 包含点 4~5(总分 70),则 score.cfg 内容为:
0 0 30 0 70
spj.cpp
可以直接套用以下框架,只修改配置区和 checkAnswer 函数即可。
#include <bits/stdc++.h>
using namespace std;
// ========== 配置区 ==========
// 编号识别方式:
// 0 = 方式A:从输入文件 .in 第一行/前几个 token 读编号
// 1 = 方式B:从输入文件 .in 前几个整数映射到编号
// 2 = 方式C:从答案文件 .ans 第一行读编号
const int ID_MODE = 2; // 按实际选择修改
// 1. Subtask 分组:每组 {起始编号, 结束编号}
vector<pair<int,int>> subtasks = {{1,3}, {4,5}}; // 请按实际修改
// 2. 编号映射(仅方式B需要)
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) {
// 方式A:直接读输入文件中的编号
int id;
if (fin >> id) return id;
return -1;
} else if (ID_MODE == 1) {
// 方式B:读输入文件前5个整数映射
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) {
// 方式C:从答案文件 .ans 第一行读编号
// 读取后 fans 的读指针会移动到编号之后,checkAnswer 可继续读标准答案
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;
}
// ---------- main ----------
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 测试点:
X,把 X 写入缓存文件 cache。
cache 读取上一侧的 X,运行 Bob,输出最终答案。
由于拆分成两个测试点会增加一倍的测试点,为减少评测压力,用 failed 文件做错误传播:只要某个偶数点答案错误,就创建 failed。后续所有测试点的 Grader
一启动就检测到 failed,直接退出;SPJ 也直接返回 0。这样后续测试点全部跳过,节省评测时间。
.in 和 .ans 文件。奇数点的 .ans 可以放占位内容,偶数点的 .ans 必须是正确答案。
0 到 100 之间的整数,表示该测试点得分。
假设原题有 N 个测试点,现在变成 2N 个测试点。编号从 1 开始:
1, 3, 5, ... 对应发送方侧。
2, 4, 6, ... 对应接收方侧。
每个原测试点 i 拆成 2i-1 和 2i。
.in
每个 .in 文件第一行格式固定为:
<测试点编号> <字符串>
例如:
1 abcdef
如果字符串里带空格,读入方式要改成 getline,这里假设字符串无空格。奇数点和偶数点的输入字符串根据题目设计,通常奇数点给 Alice 的 S,偶数点给 Bob 的 T。
.ans
score.cfg
score.cfg 每行一个整数,共 2N 行,总和必须是 100。
0。
100 / N。
例如原题 20 个测试点,则 score.cfg 共 40 行:
0 5 0 5 ...
一共 20 个 0 和 20 个 5,总和 100。
communicate.h
通信题一般要求选手只提交一个头文件,里面实现 Alice 和 Bob。Grader 也放在这个头文件里。注意:
main 函数作为评测入口。
namespace Grader 封装辅助工具,避免和选手代码符号冲突。
完整示例:
#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 {
// 通信消息 X 的最大值
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();
}
} // namespace Grader
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);
// 根据题目要求检查通信消息 X 是否合法
// 示例代码限制 X 在 [0, MAX_X] 内
if (X < 0 || X > Grader::MAX_X) {
Grader::markFailed();
return 0;
}
// 把 (id, X) 写入 cache,供下一个偶数点读取
std::ofstream cache("cache");
if (!cache) {
Grader::markFailed();
return 0;
}
cache << id << " " << X << std::endl;
cache.close();
// 输出 X,供 SPJ 参考;这一侧权重为 0,实际不计分
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
选手提交的代码大概长这样:
#include "communicate.h"
int Alice(std::string S) {
// 根据 S 计算通信消息 X
return 0;
}
int Bob(std::string T, int X) {
// 根据 T 和 X 计算最终答案
return 0;
}
MAX_X 要根据题目实际约束修改。示例里限制 X 在 [0, 1048575](即 (1 << 20) - 1)内,超出范围 Grader 会标记失败。
spj.cpp
SPJ 作为 OJ 的评测插件,职责是检查 Grader 的输出,并配合 failed 文件实现错误传播。参数固定为 argv[1] 输入文件,argv[2] 选手输出,argv[3] 标准答案。
完整示例:
#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[]) {
// 至少需要 3 个参数:输入文件、选手输出、标准答案
if (argc < 4) return 0;
// 若已有失败标记,直接返回 0
if (hasFailed()) return 0;
ifstream fin(argv[1]);
int id;
if (!(fin >> id)) {
markFailed();
return 0;
}
fin.close();
// 奇数编号(Alice侧)—— 直接判正确,返回 100(权重为 0,最终得 0 分)
if (id % 2 == 1) {
return 100;
}
// 偶数编号(Bob侧)—— 比对答案
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 为例:
Alice,得到 X,检查合法后写入 cache。id 是奇数,返回 100。score.cfg 第一行是 0,所以实际得分 0。
cache 读取上一侧的 X,调用 Bob,输出答案。communicate2.out 和 communicate2.ans。score.cfg 第二行是 5,得 5 分。
markFailed(),写入 failed 文件,返回 0,得 0 分。
failed,那么测试点 3 的 Grader 一启动就会检测到 failed,直接 return 0,不产生任何输出。failed,直接返回 0。COGS 虽然很老,但正是这种“老”为其赋予了无穷的拓展性。
| 关于 配置子任务 的讨论 | ||||
|---|---|---|---|---|
|
|
可是这是最简单的配置方法了呜呜呜
2026-09-11 22:48:38
1楼
|
|||