[语言月赛 202212] 打 ACM 最快乐的就是滚榜读队名了 (Easy Version) 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
本题要求根据一场 ICPC 比赛的完整提交记录,模拟封榜结束后的滚榜环节,依次输出滚榜嘉宾念到的每一个队名。核心解法是用优先队列维护排行榜并让堆顶始终是最后一名,每轮取出最后一名念出队名后按题号从小到大逐题揭晓它的待判题,每揭晓一题立即与当前倒数第二名比较名次,名次上升就把该队放回队列并重新从榜尾开始。
分析
核心观察
一次揭晓只会让被揭晓的队伍通过题数增加 、罚时增加,因此它在排行榜上只会前进、不会后退,其余队伍之间的相对名次完全不受影响。于是可以断定:一支队伍被念到后,若把待判题全部揭晓仍追不上当时的倒数第二名,它在剩余队伍中从此永远垫底,可以直接从排行榜中移除,不必再被念到。
思路
处理提交记录要区分封榜前与封榜后两段时间。封榜区间是 ,即时间
满足
,或
且
不同时为
;
的提交仍然计入封榜前的排行榜。
封榜前的提交直接结算。罚时只由已通过的题目决定:某题在 分钟通过时贡献
分钟,其中
是该题在这次通过之前未通过提交的次数。换算成分钟时秒数直接丢弃,例如
计
分钟。一直未通过的题目不贡献任何罚时,这一点在滚榜时同样成立:这类题目不会被揭晓,也就永远不会结算。
由此,每支队伍每道题只需要一个整数状态:该题通过之前累计的未通过提交次数;一旦通过就把它标记为已通过,此后该队对这一题的提交无论结果如何都不再影响通过结果与罚时,可以直接跳过。封榜后的通过不能立即结算,需要另开一张表记下“该题在封榜后首次通过的时刻”,等到滚榜揭晓时再连同该题此前的未通过提交一起结算。
滚榜过程用优先队列维护排行榜。名次比较的关键字依次是:通过题数多者靠前;相同则罚时小者靠前;再相同则第一次出现在提交记录中的队伍靠前。编号唯一,所以这是一个全序;把“名次更靠前”写成比较器后,堆顶恰好是最后一名。
每一轮先弹出堆顶 并输出其队名,再弹出新的堆顶
,也就是此时场上的倒数第二名。随后按题号从
A 到最后一题扫描 的待判题:每揭晓一道,就把该题的通过时刻与未通过提交的
分钟罚时一并计入,并立即用比较器判断
是否已经优于
。一旦优于,说明名次上升,剩余的待判题留到下次念到它时再揭晓,本轮结束;若扫完都没能优于
,说明
已坐稳名次,不再放回队列。无论哪种情况,
都要放回队列。
朴素做法是每揭晓一道题就把 支队伍重新排序,单次开销
;交给优先队列后每次只需
,这也是本题 Hard Version 的必需写法。
具体示例
以题面样例(,
,
)为例,封榜前的结算结果如下。
| 队伍 | 封榜前的提交 | 封榜后的提交 | 封榜时(题数,罚时) |
|---|---|---|---|
abc |
A 题 |
B 题 |
|
bcd |
A 题 |
无 |
队伍 abc 的 A 题罚时为 分钟,队伍
bcd 为 分钟;封榜后
abc 通过了 B 题,待判时刻记为 分钟。
封榜时两队题数相同、abc 罚时更大,因此 abc 是最后一名,先被念到。揭晓 B 题后它的成绩变为 ,通过题数超过
bcd 的 题,名次上升到第一位,于是被放回队列。之后念到
bcd,它没有待判题,名次不变,不再被念到。最后念到 abc,此时队列中只剩它一支队伍,滚榜结束。
输出依次为 abc、bcd、abc,与样例一致。
算法步骤
- 读入
n、m、K,准备队伍数组、记录未通过提交次数的fails表与记录封榜后通过时刻的pending表。 - 依次处理每条提交记录:解析出小时
hh、分钟mm、秒ss、题号下标p、队名name与评测结果verdict;若name第一次出现,为它分配编号id,编号即出现顺序。 - 若
fails[id][p]为(该题已通过)或
pending[id][p]非零(该题已有封榜后的通过),跳过本条记录。 - 若
verdict以A开头,即结果为Accepted:处于封榜时间(hh大于,或
hh等于且
mm、ss不同时为)时,令
pending[id][p]为hh加mm;否则令通过题数加、罚时增加
hh加mm加fails[id][p],并把fails[id][p]置为。
- 否则令
fails[id][p]加。
- 把所有出现过的队伍压入优先队列
heap,比较器按通过题数、罚时、编号决定“名次更靠前”,于是堆顶是最后一名。 - 循环:弹出堆顶
now并输出其队名;若heap已空则结束。再弹出新的堆顶nxt。 - 按题号从小到大扫描下标
:若
pending[now][p]非零且fails[now][p]不为,则令通过题数加
、罚时增加
pending[now][p]加fails[now][p],并把fails[now][p]置为。
- 每揭晓一道后就比较
now与nxt:若now更靠前,则停止揭晓(剩余待判题留到下次),并把now压回heap。 - 把
nxt压回heap,回到第步。
复杂度分析
- 时间:设念出队名的总次数为
。每一轮要么至少揭晓一道题,要么永久移除一支队伍,而每道题至多被揭晓一次、每支队伍至多被移除一次,故
;每轮扫描至多
道题,堆操作
,总时间为
,即
。
- 空间:两张
的表加上输出缓冲,为
。
实现注意事项
- 封榜从
开始,因此
的提交仍要计入排行榜;题面保证
且
时
,所以只需判断提交是否落在
内。
- 罚时以分钟为单位,通过时刻的秒数直接丢弃。
- 只有通过的题目的未通过提交才计罚时;一直没通过的题目即便有大量未通过提交也不产生罚时,滚榜时也不会被揭晓。
- 同一题通过之后(包括封榜后的通过),该队对这一题的所有提交都直接跳过。
- 名次比较必须有第三个关键字:队伍第一次出现在提交记录中的顺序。缺少它时并列队伍的顺序不确定,与题面的规定不符。
- 判定评测结果只需看首字母:六种结果中只有
Accepted以A开头。 - 评测结果含空格:先用格式化读入取出时间、题号、队名三段,再按行读入剩余部分作为评测结果,并去掉其前导空格。
- 队伍只在开始时整体压入队列一次;其后每轮中,
名次上升才压回,
无论如何都要压回。
- 堆中只剩一支队伍时输出其队名后立即结束,不要再多弹一次堆顶。
- 一次提交都没有的队伍不会出现在排行榜上,不需要为它建立条目。
- 题目数
不超过
,但题号是
A到Z的大写字母,第二维开到更省心。
源代码
#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()
评论