[语言月赛 202212] 打 ACM 最快乐的就是滚榜读队名了 (Easy Version) 的题解


记住只在没有思路时使用题解,不要从它复制粘贴代码。请尊重题目和题解的作者。
在解题之前提交题解的代码会导致封禁。

作者: admin

概述

本题要求根据一场 ICPC 比赛的完整提交记录,模拟封榜结束后的滚榜环节,依次输出滚榜嘉宾念到的每一个队名。核心解法是用优先队列维护排行榜并让堆顶始终是最后一名,每轮取出最后一名念出队名后按题号从小到大逐题揭晓它的待判题,每揭晓一题立即与当前倒数第二名比较名次,名次上升就把该队放回队列并重新从榜尾开始。

分析
核心观察

一次揭晓只会让被揭晓的队伍通过题数增加 1、罚时增加,因此它在排行榜上只会前进、不会后退,其余队伍之间的相对名次完全不受影响。于是可以断定:一支队伍被念到后,若把待判题全部揭晓仍追不上当时的倒数第二名,它在剩余队伍中从此永远垫底,可以直接从排行榜中移除,不必再被念到。

思路

处理提交记录要区分封榜前与封榜后两段时间。封榜区间是 4:00:01 \sim 5:00:00,即时间 x:yy:zz 满足 x > 4,或 x = 4 且 yy, zz 不同时为 0;4:00:00 的提交仍然计入封榜前的排行榜。

封榜前的提交直接结算。罚时只由已通过的题目决定:某题在 T 分钟通过时贡献 T + 20 \times f 分钟,其中 f 是该题在这次通过之前未通过提交的次数。换算成分钟时秒数直接丢弃,例如 0:19:38 计 19 分钟。一直未通过的题目不贡献任何罚时,这一点在滚榜时同样成立:这类题目不会被揭晓,也就永远不会结算。

由此,每支队伍每道题只需要一个整数状态:该题通过之前累计的未通过提交次数;一旦通过就把它标记为已通过,此后该队对这一题的提交无论结果如何都不再影响通过结果与罚时,可以直接跳过。封榜后的通过不能立即结算,需要另开一张表记下“该题在封榜后首次通过的时刻”,等到滚榜揭晓时再连同该题此前的未通过提交一起结算。

滚榜过程用优先队列维护排行榜。名次比较的关键字依次是:通过题数多者靠前;相同则罚时小者靠前;再相同则第一次出现在提交记录中的队伍靠前。编号唯一,所以这是一个全序;把“名次更靠前”写成比较器后,堆顶恰好是最后一名。

每一轮先弹出堆顶 now 并输出其队名,再弹出新的堆顶 nxt,也就是此时场上的倒数第二名。随后按题号从 A 到最后一题扫描 now 的待判题:每揭晓一道,就把该题的通过时刻与未通过提交的 20 分钟罚时一并计入,并立即用比较器判断 now 是否已经优于 nxt。一旦优于,说明名次上升,剩余的待判题留到下次念到它时再揭晓,本轮结束;若扫完都没能优于 nxt,说明 now 已坐稳名次,不再放回队列。无论哪种情况,nxt 都要放回队列。

朴素做法是每揭晓一道题就把 m 支队伍重新排序,单次开销 O(m \log m);交给优先队列后每次只需 O(\log m),这也是本题 Hard Version 的必需写法。

具体示例

以题面样例(n = 2,m = 2,K = 4)为例,封榜前的结算结果如下。

队伍 封榜前的提交 封榜后的提交 封榜时(题数,罚时)
abc A 题 0:00:01 不通过,0:00:02 通过 B 题 4:18:22 通过 (1, 20)
bcd A 题 0:19:38 通过 无 (1, 19)

队伍 abc 的 A 题罚时为 0 + 20 \times 1 = 20 分钟,队伍 bcd 为 19 分钟;封榜后 abc 通过了 B 题,待判时刻记为 4 \times 60 + 18 = 258 分钟。

封榜时两队题数相同、abc 罚时更大,因此 abc 是最后一名,先被念到。揭晓 B 题后它的成绩变为 (2, 20 + 258) = (2, 278),通过题数超过 bcd 的 1 题,名次上升到第一位,于是被放回队列。之后念到 bcd,它没有待判题,名次不变,不再被念到。最后念到 abc,此时队列中只剩它一支队伍,滚榜结束。

输出依次为 abc、bcd、abc,与样例一致。

算法步骤
  1. 读入 n、m、K,准备队伍数组、记录未通过提交次数的 fails 表与记录封榜后通过时刻的 pending 表。
  2. 依次处理每条提交记录:解析出小时 hh、分钟 mm、秒 ss、题号下标 p、队名 name 与评测结果 verdict;若 name 第一次出现,为它分配编号 id,编号即出现顺序。
  3. 若 fails[id][p] 为 -1(该题已通过)或 pending[id][p] 非零(该题已有封榜后的通过),跳过本条记录。
  4. 若 verdict 以 A 开头,即结果为 Accepted:处于封榜时间(hh 大于 4,或 hh 等于 4 且 mm、ss 不同时为 0)时,令 pending[id][p] 为 60 \times hh 加 mm;否则令通过题数加 1、罚时增加 60 \times hh 加 mm 加 20 \times fails[id][p],并把 fails[id][p] 置为 -1。
  5. 否则令 fails[id][p] 加 1。
  6. 把所有出现过的队伍压入优先队列 heap,比较器按通过题数、罚时、编号决定“名次更靠前”,于是堆顶是最后一名。
  7. 循环:弹出堆顶 now 并输出其队名;若 heap 已空则结束。再弹出新的堆顶 nxt。
  8. 按题号从小到大扫描下标 0, 1, \dots, n - 1:若 pending[now][p] 非零且 fails[now][p] 不为 -1,则令通过题数加 1、罚时增加 pending[now][p] 加 20 \times fails[now][p],并把 fails[now][p] 置为 -1。
  9. 每揭晓一道后就比较 now 与 nxt:若 now 更靠前,则停止揭晓(剩余待判题留到下次),并把 now 压回 heap。
  10. 把 nxt 压回 heap,回到第 7 步。
复杂度分析
  • 时间:设念出队名的总次数为 R。每一轮要么至少揭晓一道题,要么永久移除一支队伍,而每道题至多被揭晓一次、每支队伍至多被移除一次,故 R \le K + m + 1;每轮扫描至多 n 道题,堆操作 O(\log m),总时间为 O(K + (K + m)(n + \log m)),即 O((K + m)(n + \log m))。
  • 空间:两张 m \times n 的表加上输出缓冲,为 O(nm + K)。
实现注意事项
  • 封榜从 4:00:01 开始,因此 4:00:00 的提交仍要计入排行榜;题面保证 x \le 5 且 x = 5 时 yy = zz = 0,所以只需判断提交是否落在 4:00:01 \sim 5:00:00 内。
  • 罚时以分钟为单位,通过时刻的秒数直接丢弃。
  • 只有通过的题目的未通过提交才计罚时;一直没通过的题目即便有大量未通过提交也不产生罚时,滚榜时也不会被揭晓。
  • 同一题通过之后(包括封榜后的通过),该队对这一题的所有提交都直接跳过。
  • 名次比较必须有第三个关键字:队伍第一次出现在提交记录中的顺序。缺少它时并列队伍的顺序不确定,与题面的规定不符。
  • 判定评测结果只需看首字母:六种结果中只有 Accepted 以 A 开头。
  • 评测结果含空格:先用格式化读入取出时间、题号、队名三段,再按行读入剩余部分作为评测结果,并去掉其前导空格。
  • 队伍只在开始时整体压入队列一次;其后每轮中,now 名次上升才压回,nxt 无论如何都要压回。
  • 堆中只剩一支队伍时输出其队名后立即结束,不要再多弹一次堆顶。
  • 一次提交都没有的队伍不会出现在排行榜上,不需要为它建立条目。
  • 题目数 n 不超过 20,但题号是 A 到 Z 的大写字母,第二维开到 26 更省心。
源代码
#include <cstdio>
#include <iostream>
#include <queue>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>

namespace {

struct Team {
    std::string name;
    int solved = 0;   // 通过题数
    int penalty = 0;  // 总罚时(分钟)
    int id = 0;       // 编号:第一次出现在提交记录中的顺序
};

// “名次更靠前”的比较器;作为优先队列的比较器时,堆顶就是最后一名。
struct BetterFirst {
    bool operator()(const Team &a, const Team &b) const {
        if (a.solved != b.solved) return a.solved > b.solved;
        if (a.penalty != b.penalty) return a.penalty < b.penalty;
        return a.id < b.id;
    }
};

}  // namespace

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n = 0, m = 0, K = 0;
    if (!(std::cin >> n >> m >> K)) return 0;

    std::vector<Team> team(m + 1);
    // fails[t][p]:第 t 队第 p 题通过之前的未通过提交数,-1 表示该题已通过
    // pending[t][p]:第 t 队第 p 题封榜后首次通过的时刻(分钟),0 表示没有
    std::vector<std::vector<int>> fails(m + 1, std::vector<int>(26, 0));
    std::vector<std::vector<int>> pending(m + 1, std::vector<int>(26, 0));

    std::unordered_map<std::string, int> teamId;
    int cnt = 0;

    std::string line;
    std::getline(std::cin, line);  // 吃掉第一行剩余的换行
    for (int k = 0; k < K; ++k) {
        if (!std::getline(std::cin, line)) break;
        std::istringstream iss(line);
        std::string timeToken, problemToken, name, verdict;
        iss >> timeToken >> problemToken >> name;
        std::getline(iss, verdict);
        const std::size_t begin = verdict.find_first_not_of(' ');
        verdict = (begin == std::string::npos) ? std::string() : verdict.substr(begin);

        int hh = 0, mm = 0, ss = 0;
        std::sscanf(timeToken.c_str(), "%d:%d:%d", &hh, &mm, &ss);
        const int p = problemToken[0] - 'A';

        auto it = teamId.find(name);
        if (it == teamId.end()) {  // 新队伍:编号就是出现顺序
            teamId[name] = ++cnt;
            it = teamId.find(name);
            team[cnt].name = name;
            team[cnt].id = cnt;
        }
        const int t = it->second;

        if (fails[t][p] == -1 || pending[t][p] != 0) continue;  // 该题已有结论

        if (!verdict.empty() && verdict[0] == 'A') {  // 只有 Accepted 以 A 开头
            if (hh > 4 || (hh == 4 && (mm > 0 || ss > 0))) {
                pending[t][p] = hh * 60 + mm;  // 封榜后的通过留到滚榜时结算
            } else {
                team[t].solved += 1;
                team[t].penalty += hh * 60 + mm + 20 * fails[t][p];
                fails[t][p] = -1;
            }
        } else {
            fails[t][p] += 1;
        }
    }

    std::priority_queue<Team, std::vector<Team>, BetterFirst> heap;
    for (int i = 1; i <= cnt; ++i) heap.push(team[i]);

    std::string out;
    while (!heap.empty()) {
        Team now = heap.top();  // 最后一名
        heap.pop();
        out += now.name;
        out += '\n';
        if (heap.empty()) break;  // 只剩一支队伍,念完即结束

        Team nxt = heap.top();  // 场上的倒数第二名
        heap.pop();

        bool rose = false;
        for (int p = 0; p < n; ++p) {  // 从 A 题依次揭晓
            if (pending[now.id][p] == 0 || fails[now.id][p] == -1) continue;
            now.solved += 1;
            now.penalty += pending[now.id][p] + 20 * fails[now.id][p];
            fails[now.id][p] = -1;  // 已揭晓的题不会再次揭晓
            if (BetterFirst()(now, nxt)) {  // 名次上升
                rose = true;
                break;
            }
        }
        if (rose) heap.push(now);  // 还会被念到
        heap.push(nxt);            // 无论是否上升,倒数第二名都要放回
    }

    std::fwrite(out.data(), 1, out.size(), stdout);
    return 0;
}
import heapq
import sys


def main():
    data = sys.stdin.buffer.read().split(b"\n")
    if not data or not data[0].strip():
        return
    n, m, K = map(int, data[0].split())

    solved = [0] * (m + 1)
    penalty = [0] * (m + 1)
    fails = [[0] * 26 for _ in range(m + 1)]    # -1 表示该题已通过
    pending = [[0] * 26 for _ in range(m + 1)]  # 0 表示没有待判的通过

    index = {}
    names = [b""] * (m + 1)
    cnt = 0

    for i in range(1, K + 1):
        parts = data[i].split()
        hh, mm, ss = map(int, parts[0].split(b":"))
        p = parts[1][0] - ord("A")
        name = parts[2]
        accepted = parts[3][0:1] == b"A"  # 只有 Accepted 以 A 开头

        if name not in index:  # 新队伍:编号就是出现顺序
            cnt += 1
            index[name] = cnt
            names[cnt] = name
        t = index[name]

        if fails[t][p] == -1 or pending[t][p]:
            continue

        if accepted:
            if hh > 4 or (hh == 4 and (mm > 0 or ss > 0)):
                pending[t][p] = hh * 60 + mm
            else:
                solved[t] += 1
                penalty[t] += hh * 60 + mm + 20 * fails[t][p]
                fails[t][p] = -1
        else:
            fails[t][p] += 1

    # 元组 (题数, -罚时, -编号) 越大名次越靠前,于是堆顶(最小值)就是最后一名
    heap = [(solved[t], -penalty[t], -t, t) for t in range(1, cnt + 1)]
    heapq.heapify(heap)

    out = []
    while heap:
        now = heapq.heappop(heap)  # 最后一名
        out.append(names[now[3]])
        if not heap:
            break  # 只剩一支队伍,念完即结束
        nxt = heapq.heappop(heap)  # 场上的倒数第二名

        s, pen, t = now[0], -now[1], now[3]
        rose = False
        for p in range(n):  # 从 A 题依次揭晓
            if pending[t][p] == 0 or fails[t][p] == -1:
                continue
            s += 1
            pen += pending[t][p] + 20 * fails[t][p]
            fails[t][p] = -1
            if (s, -pen, -t) > (nxt[0], nxt[1], nxt[2]):  # 名次上升
                rose = True
                break
        if rose:
            heapq.heappush(heap, (s, -pen, -t, t))
        heapq.heappush(heap, nxt)

    if out:
        sys.stdout.buffer.write(b"\n".join(out) + b"\n")


if __name__ == "__main__":
    main()

评论

目前没有评论。