BaoTx's Blog
目录

COGS Subtask 通信题 配置指南

COGS Subtask 通信题 配置指南

Subtask 和通信题的配置均利用了 COGS 是串行评测且单次评测目录不变的特性。

本文章部分介绍内容由 AI 代为撰写/润色,所有内容均经过人工核查,如果不对地方请见谅。

捆绑测试(Subtask)配置指南

核心原理

评测系统对每个测试点独立运行一次 SPJ。我们利用 SPJ 可以读写本地文件的特性,用一个临时缓存文件(例如 .subtask_cache)记录每个测试点是否通过。

把每个 Subtask 的最后一个测试点设为“出口点”。运行到出口点时,SPJ 去读缓存,汇总本组所有点的通过状态。如果全对,返回 100(即该组总分配分),否则返回 0。非出口点只根据自身正确性返回 100 或 0,但在 score.cfg 里配置为 0 分,所以不会影响总分。

spj.cpp 的返回值必须是 0100 之间的整数,表示该测试点得分。所有判定由评测系统自动执行。

第一步:数据准备

1. 识别测试点编号(三选一)

SPJ 需要知道当前在评测哪个点。建议根据题目情况选择一种方式:

方式 A:在 .in 文件中显式放置编号
在每个 .in 文件的第一行(或前几个 token)放一个整数,例如 12。SPJ 从输入流直接读取这个编号。适用于允许修改输入格式的题目。

方式 B:通过输入内容映射
提取每个 .in 文件前几个固定的整数作为“指纹”,在 SPJ 里用 map 把指纹映射到编号。适用于不想修改输入格式,且前几个整数组合能唯一区分测试点的情况。

方式 C:在 .ans 文件第一行显式放置编号
在每个 .ans 文件第一行写该测试点编号,后面才是标准答案。例如:

1
2
1
42

SPJ 先读第一行的编号,读指针自然移动到编号之后,checkAnswer 直接从当前位置继续读标准答案即可。这种方式需要修改所有 .ans 文件,且 .ans 第一行不能再作为标准答案内容。

2. 编写 score.cfg

在题目文件夹下建立 score.cfg,按测试点顺序每行一个整数。所有测试点分数总和必须是 100 分

配置规则:除了每个 Subtask 的最后一个测试点(出口点)外,其余点都设为 0;出口点设为该 Subtask 的总分。

示例:Subtask1 包含点 13(总分 30),Subtask2 包含点 45(总分 70),则 score.cfg 内容为:

1
2
3
4
5
0
0
30
0
70

第二步:编写 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;

// ========== 配置区 ==========

// 编号识别方式:
// 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 测试点:

  • 奇数编号测试点:运行 Alice,计算 X,把 X 写入缓存文件 cache
  • 偶数编号测试点:从 cache 读取上一侧的 X,运行 Bob,输出最终答案。

由于拆分成两个测试点会增加一倍的测试点,为减少评测压力,用 failed 文件做错误传播:只要某个偶数点答案错误,就创建 failed。后续所有测试点的 Grader 一启动就检测到 failed,直接退出;SPJ 也直接返回 0。这样后续测试点全部跳过,节省评测时间。

前提条件

  • 每个测试点要有 .in.ans 文件。奇数点的 .ans 可以放占位内容,偶数点的 .ans 必须是正确答案。
  • SPJ 返回值是 0100 之间的整数,表示该测试点得分。

第一步:数据准备

1. 拆分测试点

假设原题有 N 个测试点,现在变成 2N 个测试点。编号从 1 开始:

  • 奇数编号 1, 3, 5, ... 对应发送方侧。
  • 偶数编号 2, 4, 6, ... 对应接收方侧。

每个原测试点 i 拆成 2i-12i

2. 准备输入文件 .in

每个 .in 文件第一行格式固定为:

1
<测试点编号> <字符串>

例如:

1
1 abcdef

如果字符串里带空格,读入方式要改成 getline,这里假设字符串无空格。奇数点和偶数点的输入字符串根据题目设计,通常奇数点给 Alice 的 S,偶数点给 Bob 的 T

3. 准备答案文件 .ans

  • 奇数点:不需要标准答案,随便占位。
  • 偶数点:放 Bob 应该输出的正确答案。

4. 编写 score.cfg

score.cfg 每行一个整数,共 2N 行,总和必须是 100。

  • 奇数行(发送方侧)全部写 0
  • 偶数行(接收方侧)写 100 / N

例如原题 20 个测试点,则 score.cfg 共 40 行:

1
2
3
4
5
0
5
0
5
...

一共 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 {

// 通信消息 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

选手提交的代码大概长这样:

1
2
3
4
5
6
7
8
9
10
11
#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] 标准答案。

完整示例:

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[]) {
// 至少需要 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 为例:

  1. 测试点 1(奇数)
    Grader 调用 Alice,得到 X,检查合法后写入 cache
    SPJ 发现 id 是奇数,返回 100。
    因为 score.cfg 第一行是 0,所以实际得分 0。

  2. 测试点 2(偶数)
    Grader 从 cache 读取上一侧的 X,调用 Bob,输出答案。
    SPJ 比对 communicate2.outcommunicate2.ans

    • 正确:返回 100,score.cfg 第二行是 5,得 5 分。
    • 错误:调用 markFailed(),写入 failed 文件,返回 0,得 0 分。
  3. 错误传播
    如果测试点 2 写了 failed,那么测试点 3 的 Grader 一启动就会检测到 failed,直接 return 0,不产生任何输出。
    测试点 3 的 SPJ 也会检测到 failed,直接返回 0。
    后续所有测试点都会这样跳过,直到评测结束。

COGS 虽然很老,但正是这种“老”为其赋予了无穷的拓展性。

对应页面:[COGS]配置子任务 - COGS