[COGS]配置子任务

syzhaoss 在 2026-09-11 创建  开放分组:全部用户  上次编辑时间:2026-09-11

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
42

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

2. 编写 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 测试点:

  • 奇数编号测试点:运行 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 abcdef

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

3. 准备答案文件 .ans

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

4. 编写 score.cfg

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

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

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

0
5
0
5
...

一共 20 个 0 和 20 个 5,总和 100。

第二步:编写 Grader 头文件 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 为例:

  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 虽然很老,但正是这种“老”为其赋予了无穷的拓展性。

关于 配置子任务 的讨论
Gravatar
ChenBp
积分:1706
提交:336 / 858
可是这是最简单的配置方法了呜呜呜

2026-09-11 22:48:38 1楼