2020年了,为了 blog 不咕,还是要写点什么。

这是一个补档,记录了 2019 CUSTACM 校队选拔赛前为了校史上首次推出的滚榜活动做的准备。

不过说到为什么要做这个,其实都是一时兴起罢了。

该方案可以被广泛应用于各种 OJ 而不限于 DOMJudge 或 PC^2。我校的 CustOJ 就是基于 OnlineJudge 的。

什么是 ICPC Resolver

ICPC Resolver 是 ICPC Tools 中的一员,最初都是用于 ICPC World Finals 的。

如同它一万年不更新的文档所说:

The ICPC Resolver is a tool for graphical animation of contest results. It shows the final runs submitted

during a contest in an interesting way, and leads up to display of the award winners.

ICPC Resolver 可以在比赛结束后,用于展示封榜时期内的比赛结果变化,也就是俗称的滚榜。

长期以来,在 *cpc 现场赛,滚榜都是一项传统艺能。不过也有的赛区因为各种原因不滚榜/滚不了榜,令人发指(指省赛)

如果身为选手,观看滚榜是非常激动人心的,尤其是在铜铁/银铜/金银交界的区域,看到自己队名的时候。当然,作为万年铜首(Cust),我已经麻木了。所以才有勇气直视 Resolver(雾)

如何部署 ICPC Resolver

进入官网,点击链接,一键下载,解压运行,完成

No~ No~ No~

下载解压后你会发现,他连个能跑的 demo 都不给,这怎么整?

至于文档,乍一看运行方法倒是有很多,什么 Event Feed 啦,CDP/CDS 之类的,仔细阅读就会陷入递归读文档的深渊中。更过分的是,向前挖了几个版本,文档居然长得都几乎一毛一样。至少从文档上,并看不出历代 Resolver 的具体数据要求的区别。

而真实的 Resovler 文档,大概散落在 PC^2 文档里的某个角落。当然,PC^2 给的文档,只介绍了怎么把 PC^2 的数据喂给 Resolver。DOMJudge 大概也同理。

于是求助搜索引擎。根据我们的实际需求,排除了 CDP 的运行方式,最后参考了这篇博文里提供的数据。此处要感谢东北大学。

题外话: 这个数据来自16沈阳站热身,记录了我的第一口大锅: 帮学长把一个最短路 sb 题读成了一个不可做题

如何正确运行 ICPC Resolver

数据格式

虽然有了数据,一开始也是跑不起来。根据 Exception 和凌乱的文档瞎猜了一些改法(比如把 run 改成 submission,跑是能跑起来了,不过跑起来是一个完全没有数据,只有队名的情况。中间略去许多的坑,得到的结论是: 使用了过高版本的 Resolver (干!)

枚举了数个过往的 dev 版本,最后确定了使用 1.1.0dev.1057。这个版本可以正确兼容目前手里的数据。

之后做的事情,其实是对网上下载来的数据做一个瘦身。不负责任的猜测,那份数据是 PC^2 自动生成的。再略去来回反复枚举参数的操作,最后得到了如下的结构

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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<contest>
<info>
<title>A Contest</title>
<length>05:00:00</length>
<scoreboard-freeze-length>01:00:00</scoreboard-freeze-length>
<penalty>20</penalty>
</info>
<problem>
<id>1</id>
</problem>
<problem>
<id>2</id>
</problem>
<problem>
<id>3</id>
</problem>
<problem>
<id>4</id>
</problem>
<team>
<id>121</id>
<university>队伍名称1</university>
</team>
<team>
<id>122</id>
<university>没有队</university>
</team>
<team>
<id>123</id>
<university>比速度</university>
</team>
<team>
<id>124</id>
<university>有情绪</university>
</team>
<team>
<id>125</id>
<university>好选拔</university>
</team>
<run>
<id>3</id>
<problem>1</problem>
<team>121</team>
<time>110.523</time>
<judged>true</judged>
<solved>true</solved>
<penalty>false</penalty>
<first-to-solve>true</first-to-solve>
</run>
<run>
<id>4</id>
<problem>1</problem>
<team>122</team>
<time>152.958</time>
<judged>true</judged>
<solved>true</solved>
<penalty>false</penalty>
</run>
<run>
<id>5</id>
<problem>1</problem>
<team>123</team>
<time>153.628</time>
<judged>true</judged>
<solved>false</solved>
<penalty>true</penalty>
</run>
<run>
<id>6</id>
<problem>1</problem>
<team>124</team>
<time>14556.691</time>
<judged>true</judged>
<solved>false</solved>
<penalty>true</penalty>
</run>
<run>
<id>7</id>
<problem>1</problem>
<team>125</team>
<time>4157.132</time>
<judged>true</judged>
<solved>true</solved>
<penalty>false</penalty>
</run>
<run>
<id>8</id>
<problem>1</problem>
<team>1</team>
<time>1180.523</time>
<judged>true</judged>
<solved>true</solved>
<penalty>true</penalty>
</run>

<award>
<team>121</team>
<type>winner</type>
<citation>Champion</citation>
</award>
<award>
<team>122</team>
<type>medal</type>
<citation>Not Gold Medalist</citation>
</award>
<award>
<team>123</team>
<type>medal</type>
<citation>铁牌</citation>
</award>
<award>
<team>125</team>
<type>medal</type>
<citation>Silver Medalist - 银奖</citation>
</award>
<award>
<team>124</team>
<type>medal</type>
<citation>Bronze Medalist</citation>
</award>
<award>
<team>121</team>
<type>first_to_solve</type>
<citation>A题一血</citation>
</award>
</contest>

首先是比赛的信息,标题、总时长、封榜时间、罚时信息是必要的。

之后是题目,每个题目给个id就行。

再之后是队伍信息,每个队伍有个id,而队伍名称用university代替,可能是因为那个版本的 Resolver 只滚学校名称(毕竟 World Finals)。

run代表提交信息(这部分新版本已经完全不一样了),每次提交有个id(使用整数类型,而不是 hash string)。problemteam代表该次提交哪支队伍了哪题,里面填的是对应的id,很好理解。time是从 0 开始以秒为单位的时间戳。judged=true是必要的,否则会抛异常。solvedpenalty是两个属性,合在一起可以代表提交的结果。只有 Accepted 中的solved=truepenalty=false,其他都是提交不通过,solved=false。其中 Compilation Error 不计罚时,所以penalty=false,否则penalty=true。最后可能有first-to-solve=true的属性,代表该次提交是该题的一血提交。但是不需要first-to-solve=false,切记。

award代表奖项,这里需要人工颁下奖。team代表获奖队伍的idtypewinnermedalfirst_to_solve三种,citation是获奖的详细介绍。一支队伍允许获三个type不同的奖。

中文字体

老外开发的软件,对中文支持不是很好(等于没有)。Resolver 默认使用的是 Helvetica,一个古老而经典的拉丁字母无衬线字体。

根据搜索引擎,据说支持通过设置环境变量ICPC_FONT来调整使用的字体,但实测没有效果。估计是高版本 Resolver 的 feature。

不过,通过解压resolver.jar包发现,字体是打包在里面的。只要将里面的字体替换成有中文的,就有办法支持中文。没有编码问题真是太感动了

原本的字体文件名叫HELV.PFBpfb是一个古老的字体格式。将我们已有的字体(基本上都是ttf格式)通过在线工具转成pfb,并命名成HELV.PFB放在font文件夹下,之后通过以下命令即可将我们想要的字体注入进 jar 包。

1
jar -uvf resolver.jar font

略过一番折腾和反复重试(不是所有中文字体都很好地适配 Resolver,非常容易出现文字重叠/截断或其他奇奇怪怪的事情),使用了自己的 Mac 里不知道哪里掏出来的STHEITI.ttf。这个字体看起来效果还 OK。

不过还是有问题。有一些特殊的字符,显示效果不好,比如提交信息中间的-(两边貌似有空格)。于是尝试组合字体。

随手找了一个合并字体文件的在线工具,将原本的HELV.PFB里的字体(需要先转成ttf)和STHEITI.ttf合并。

由于 Helvetica 有点古老,为了显示效果,使用 Right Join,即以STHEITI.ttf为主,没有的字符从HELV.PFB获取,得到的mixed.ttf转成pfb注入resolver.jar

经过这样一番折腾,终于得到了可用的 Resolver。这里提供已经折腾完的版本 - Github

运行

首先我们假设文档说的是真的。所以通过命令启动 Resolver。

1
./resolver.sh sample.xml

我只验证了 fast 参数,是好用的,例如这样加速 10 倍滚榜

1
./resolver.sh sample.xml --fast 0.1

其他参数可能也是真的。以上命令适用于 Linux/MacOS,Windows 使用resolver.bat同理,毕竟是跨平台的 Java 项目。

另外,滚榜中的一些键盘操作,也可能是真的。反正按空格真的可以让它一直滚。

如何在 CustOJ 上实装

其实有了 Resolver 所需的数据格式,剩下的只是大模拟而已。把 OJ 的题目和提交记录,通过字符串处理成所需的格式,然后再手动填一下谁获了奖,滚榜就算 ready 了。

No~ No~ No~

不知道怎么就从大模拟变成了一坨 SQL (大草

大概是有点懒得加接口专门搞这玩意,并且也不想写个爬虫(自己爬自己是怎么回事)再把数据 format 起来,所以最简单粗暴的方法当然是~操库!

有一说一,PostgreSQL 还是挺好操的。首先它原生支持查询结果以xml格式返回,这就方便了许多。对应的命令xmlforest。其他 RDBMS 也同理,感觉基本都有。

之后就是写查询了。偶尔把逻辑写在 SQL,感觉也不错,反正上面也没有其他语言对接,直接就出结果了。

这里需要两条查询,一条查询所有的参赛队伍,还有一条查询所有的提交。当然比较流氓的写法就是两条查询UNION ALL起来,那搞一次就行了。

整句SQL写了不到20行,可以接受。为避免透露人尽皆知的 CustOJ 表结构,这里就不放了。不过 Github Repo 里有这段 SQL。

查询的逻辑,按实际需求做就行,要注意的点大概有这几条

  • 筛选有提交的参赛队伍,以防滚榜从一堆鸽子开始
  • 提交记录过滤掉验题的(如果有的话)
  • 查一血的时候,不如多 join 一次,而不是嵌套查询

对于 PostgreSQL 来说,查询结果保存在文件挺方便的,利用\o指令即可。

如果 DB 端口不对外开的话,这些查询可以在服务器上做,之后scp下来(什么? 没有scp?? OJ 平时都靠人肉运维吗???)

之后把结果贴进要 resolve 的xml文件就行。注意上面说的,不需要first-to-solve=false。如果查询里避免不掉,可以批量替换成空串。

后记

虽然是后记,也是后得很后的后记了。

由于比赛举办时不在学校,所以滚榜由学弟代为操作了。我远程操作了比赛结果整理了滚榜数据。

感觉效果还行,让许多本校的、NENU/CCUT 没参加过现场赛的同学体会了滚榜这项 *cpc 传统艺能。据说整场比赛的办赛效果惊动了隔壁 NENU,引来无数大佬砸钱赞助他们的 ACM(?)。

另外,这套单独的滚榜方案显然没有 JLU - yanger 的全套 DOMJudge + ICPC Tools 来得完善。今年我校要办赛,首先是希望有滚榜吧,其次不要像去年 EC Final 一样因为技术原因把滚榜锅了。像这样的一套备用方案,也算有个底。

对了,首先的首先的首先,希望今年省赛有 Ubuntu!