AT Birthday0410 X - 论如何在 2.9k 以内解决战斗
TL;DR
本题可以以
本题可以以
前言
upd 2022/7/15: 优化到 7k 以下,原先 9k。
upd 2022/8/1: 修正了一些错误,之前的不计换行符应为换行符记为一个字符,记换行符应为换行符记两个字符;增加了新内容。
2022/8/1: 最终停在了 4k 的门槛上啊,累了。
upd 2022/8/2: www,非常抱歉食言了,发出来之后发现了一些不能容忍的错误。以及隔了一天之后我又不累了。
2022/8/2: 进入了 4k 大关。
upd 2022/9/24: 进入 3.6k 大关。
upd 2022/10/27: 修复代码错误,补档一个特性的解释。
upd 2022/11/11: 答案是优化之后的代码只能在 Windows 系统上跑。
2022/11/11: 答案是 Western Windows-1252 编码,我的评价是:笨比 Linux 的游戏理解就到这了好吧。
upd 2022/11/12:
2022/11/12: 总之我是魔怔了。
upd 2023/7/12: 随便更一句(其实是老早之前就发现的):本题可以取 之前老以为工程界的圆周率是整数是刻板印象。啊不对就是刻板印象。
2023/7/12: 还有大家别 copy 了,现在
以内的都是我代码…… 小心哪天就点国策大清洗(
upd 2023/8/11: 更新长度。
upd 2023/10/19: 更新长度。
upd 2023/12/21: 更新长度。
我寻思这题也不难啊,我才交了四发搞定一些弱智错误就有 480 了。
要 A 掉不就交了 17 发,花了两个晚上一个下午大概两位数小时么。
论如何在 23k 以内迅速地解决战斗
首先(一开始)我没有使用压缩技术把图像压缩进去。
当时写着写着,开始调了,突然发现自己好像可以(在洛谷上)整个最短解。
降噪与分离
首先因为这题是个大模拟,考虑封装一个 image
类便于操作图像:
1 |
|
众所周知我们在处理二维图的时候经常要要遍历整张图,或者对一个点去寻找与它联通的点,所以我们可以考虑对这两种东西做一个封装:
1 |
|
然鹅后来发现
expand
用的太少了,我给它优化掉了。
具体用法,比如我们要降噪,使用众数滤波器,要统计周围的合法的点数以及黑点的数量,就可以这么写:
1 |
|
众数滤波,即对每个点计算(包含自身的)周围的点的情况,选取最多的颜色进行着色。
不过这里其实可以不统计四周合法点的数量,接触边缘的点我们可以直接当作噪音。
再比如,我们调试要输出整张图像,就可以这么写:
1 |
|
这个
指写出了
charset[i].rotate(j).cut(-.1,-.1).reno().print()
的魔怔人。
然后接下来,由于我们对图像做操作,以及分离的时候需要框出图像的四边,所以我们再封装一个类:
1 |
|
分离大概长这样:
1 |
|
然后我们就得到一个由 image
组成的 vector
,下一步可以开始匹配了。
不过在此之前……
变换与预处理
首先我们先把基础字体塞进一个 vector<image>
里面,使用 C++11 的原始字符串字面量,大概像这样:
1 |
|
众所周知只有基础字体的图像(以下称“本源图像”),我们是不可能 A 掉这道题的。
由于题目给出的图像已经变换的很厉害了,所以试图对这些图像做逆变换可能不是个好主意。所以我们要对本源图像做亿些变换,使得它们更加像题目中给定的图像。
然后看到题目中给出了变换顺序:
- 整体缩小。
- 对
方向与 方向分别缩小(改变比例)。 - 旋转
度。 - 剪切变换。
其实最开始我没看到这个变换所以只拿了 480 分,看到之后就有 490 了。
所以我们的图像也要按顺序变换,先进行旋转。
这个问题是这样的,因为缩放的参数预先猜测效果并不好,但是如果其他变换都做完了,缩放的参数其实可以通过图像信息得出,所以我们不妨到了要匹配的时候再进行缩放。
旋转按照公式写即可:
1 |
|
然后是剪切变换:
1 |
|
封装的收益这个时候就体现出来了。
最后是缩小操作(强调缩小是因为放大需要额外增加黑点,比较难处理,我写的这个比较简单,只能缩小):
1 |
|
然后在预处理中搞出若干张图像来,方便处理。我没有使用随机,而是直接按照一定分度值枚举参数。
1 |
|
这里使用降噪是因为变换容易搞出中心空点。
匹配与计算
也就是主函数!
我们使用暴力匹配法,对于每一个位置进行匹配,最后通过匹配的数量除以总的数量得到匹配率。
1 |
|
那么经过了大量的封装,主函数只需要调用即可:
1 |
|
计算我是直接 copy 的,大家可以去看其他的题解(雾)。
最后一步
Final Step!
把所有的空格缩进换成 Tab (雾)。
后记
这道题我实际写起来比想的容易很多(?
一开始甚至没有看到剪切操作就拿到了 480pts。
这也证明了考场上打这个做法是非常不错的,因为好写又好拿分(?
感谢 @xiyihan 提供的字符表以及最后一步计算的代码(指我直接进行一个厚颜无耻的 copy)。
完整代码可以在 AtCoder 的提交记录中查看。
最后,Clang 吊打 GCC!
论如何在 9k 以内优雅地解决战斗
字符表压缩
首先我们观察字符表,发现这东西巨大,肯定要压缩。
我选择了游码,主要是因为它易于解码,而且这题的字符集为二,非常适合游码发挥(更好的方法解码更复杂,效果其实没有理论那么好)。
实测大约是
具体的过程,我们可以将输入看作为一维字符串进行,解码时再额外传一个宽度即可还原成二维矩阵。
1 |
|
然后我们的 16 个字符就是这样的画风:
1 |
|
这里缩了一点点,删掉了最后一个数字,即忽略掉了最后的若干个 0,因为初始就会填充为 0.
大概?我也不知道为什么没有
push_back
能行,反正过了。
2022/10/27 更新
这是一个 STL 特性,因为 vector<>
对 bool
类型特化,使用 64 位整数(取决于机子,如果是 32 位机子就 32 位整数,跟 bitset<>
一个道理),来储存信息。
这就会导致其实我们的 vector
长度如果小于 64 的话实际上对于内存而言没什么意义,因为它至少要开一个整数。
而显然的,我们字符集中的字符宽度小于 64(哪怕 32 也恰好没到),所以每一行都只会使用一个整数储存。
这就意味着,虽然我们的最后一行没有 push_back
,但是内存是存在的,所以我们在访问这些位置时不会越界,而只是会得到随机值(访问未初始化内存)。
但这无关紧要,因为它一定会被降噪干掉。
所以如果你使用 int
存,但是提前 reserve
一下的话,大概也是这个效果吧。
重构与优化
我们把
expand
优化了(因为只在类的内部调用了两次,而且 API 还不是很合适,所以给它内联掉)。我们把
get
优化了(因为只在类的内部调用了一次,所以给它内联掉)。因为
push_back
与emplace_back
太多了,所以全部替换成#define eb emplace_back
。返回值可以用
auto
。我们发现
rotate
与cut
包含大量重复代码,所以我们可以:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<class T>image trans(T func){
vector<pair<int,int>> point;
sqr out={INF,-INF,INF,-INF};
foreach([&](int x,int y){
if(!raw[x][y]) return;
auto p=func(x,y);
// 对坐标进行变换,返回一个 pair<double,double> 表示新坐标
point.eb(p);
out+=p;});
image re(out.x1-out.x0,out.y1-out.y0);
for(auto i:point)
re[i.first-out.x0][i.second-out.y0]=1;
return(re);
}然后
rotate
变成这样:1
2
3
4
5
6auto rotate(db ang){
ang=ang/180*pi;
db csa=cos(ang),sia=sin(ang),cex=rx/2.,cey=ry/2.;
return(trans([&](int x,int y){
return(make_pair((x-cex)*csa+(y-cey)*sia+cex,-(x-cex)*sia+(y-cey)*csa+cey));}));
}cut
这么写:1
2
3auto cut(db cx,db cy){
return(trans([&](int x,int y){return(make_pair(x+y*cy,y+x*cx));}));
}优化最后的计算代码。
其他非常多的优化,有兴趣的话还是翻到下面见源码吧。
最后一步
把所有的空格缩进换成 Tab (大雾)。
后记
你怎么这么多后记!
感谢 @xiyihan 的题解提供的思路 以及让我看到了这道题是可以写的。
在没有过多的牺牲代码的可读性的前提下(没有过于奇怪的变量名以及过度离谱的压行),最终得到的代码约有
用 Clang 提交最慢的数据点耗时
完整代码见提交记录。
再复读一遍愚蠢的 GCC:
Python 占据最短提交第一的历史,终于结束了。虽然我估摸着很快就会有人给我干掉。
论如何在 8k 以内解决战斗
额,我们直接进入正题吧。
但是我们并不满足于
以下“压缩组”指游码得到的一个数,即一个表示一块相同颜色的大小的数。
我们观察压缩表,发现里面有大量的一位数与二位数,实际上,使用优秀的文本编辑器我们可以快速看出里面最大的二位数是一个
而三位数并没有几个,最大的是
这启发我们可以将小数字压缩起来,我们按照
需要注意的是 unsigned long long
有
解码仍比较简单,我们对于小于
将较大的数字按照
压缩器代码:
1 |
|
解压器(即 image
类的构造函数)如下:
1 |
|
最终得到的压缩表如下:
1 |
|
最后的代码长度
完整代码仍然是见 AtCoder 上的提交记录,不过这份代码的压缩表有一点点小问题,比上面给的多了
Anything More Exciting?
到这一步我其实不大会了,但是还是有一些不那么聪明的办法继续压缩。
我们发现我们的数字都是 0x
占位符。而众所周知,ASCII 有 0x7f
(也就是说我们实际上有
表长度
1 |
|
理论上还可以做的更绝一点,我们可以将 ASCII 前面
而后面的字符会出更多的问题,它们很容易被解释为 UTF-8 编码然后导致解码失败,就算本地能行(而且其实本地也很难复制这些字符,要实现可能得直接改二进制文件),复制粘贴到浏览器环境中也很有可能寄掉。
2022/8/2 更新
事实上,由于不同 cv 限定符的同一类型作为参数允许同时出现在同一函数的重载中,所以我们可以将宽度信息当作一个压缩组编码进字符串中。
发现这样十分有效,而且把新的解码器写出来,发现解码器只多了
大概是我的码风问题。
一个更有趣的是,接下来我们将
小编也很惊讶.jpg
因为实操的时候,转换出的字符串并不能完美地直接使用,其中存在的
"\
两种字符需要转义,所以会产生一点细微的差别。原始字符串字面量可以解决这个问题,但是一个代价
有点贵。
我们发现字体的一些极小的空白会影响我们的压缩(游码的特性),但是对匹配的影响不大,所以可以滤除一些空白。
影响不一定都是负面的,需要一个一个地尝试来确定。
然后使用 unsigned __int128
!
最终,我们的表长
不过因为一些原因,以下提交记录的表长为
2022/10/27 更新
由于有了上面的结论,所以我们可以将最后一个压缩组省掉,对于最后一个压缩组为 #
,我们直接向本源图像人为添加一个 .
即可。
表长
2022/11/11 更新
我试了试,可以把所有的字符都用上,只要没有空字符都能简单搞,带空字符的话就要直接改二进制文件。
于是我们压缩有两个参数,进制(使用的最大字符数量)与位移(分隔符的 ASCII 码,从分隔符之后选取字符)。
经尝试使用 \"
与 \n
与 \r
较少),共
(虽然 总之我也觉得挺神奇的)。
但是非常不幸的是,这份压缩表并不能在 Linux 上运行(只有 Western Windows-1252 能支持这个压缩表)。
但是不论怎么样,这仍然是一个巨大的进步。
2022/11/12 更新
那怎么办呢,我没辙了。
只能考虑有损压缩了(其实之前也算是有损压缩吧,虽然损失大概可以算是忽略不计)。
我们有两条路径:
把图像缩小。
具体什么效果我不清楚,因为我走的另一条路径。
调整压缩组
不知道在什么精神状态下想出来的奇异方法。
考虑如果我们每个压缩组都是偶数的话(其实本来想的是奇数的,因为奇数的压缩组比偶数的多,但是有一个 bug 级的
所以没办法),那么我们就可以把最后一位省掉。 具体的调整方法就遇到大于
的奇数就减一,然后再遇到就加回去,然后对于 强制加一。 然后全部除二,你发现大部分的数都小于等于
,所以我们整一个 进制即可。 由于宽度都大于
,所以你发现对于第一位一定恰好是一个字符表示宽度,所以可以将其后的分隔符删去。 出来的图像是这样的:
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............####...........
...........######..........
........##########.........
.....############..........
..################.........
.################..........
##################.........
.################..........
##################.........
.######..########..........
..##......########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
..........########.........
.........########..........
###########################
###########################
###########################
###########################
###########################
#########################..但是在降噪的鼎力相助下反正是过了。
然后又研究一些语法总之是压下去了,删掉了(最后)一个优化,幸好只跑了
其实可以两种压缩方法结合一下,但是我已经把整个代码压缩得少于
压缩情况:
- Windows:
,压缩率 。 - Linux:
,压缩率 。
最后的最后
不后记了是吧。
这道题作为一道大模拟写起来其实非常舒服(至少我觉得很舒服),尤其地考验了对与 STL 库、模板等 C++ 特性的理解(大概?)。
但是耗时还是确实耗时的,而且万一思路不是非常好的话会寄的很难受。
对于图像压缩的思考确实非常有趣,我觉得下次可以出一个给定图像,要求提交程序输出该图像,按照程序长度评分这样的题目。
以下是开始的一些尝试的进程(前八份代码可读,至少本人可读):
Linux 下可运行的代码目前为
1 |
|
2022/10/27: 之前的代码出了问题,见谅,现在这个能通过所有数据。
2022/11/12:
的 提交记录,AtCoder 计算换行是按照 计算的,但这不影响我们冲进 。 2023/7/12:
的 提交记录 2023/8/11:
的 提交记录 2023/10/19:
的 提交记录 2023/12/21:
的 提交记录
Windows 下可运行的代码目前为
1 |
|
本当の本当に終わり
后附文件
感谢你看到这里。
如果想尝试这题,以下可能有帮助。
不再维护的某个版本的最短代码可读版(去除了压缩表,包含两种解码,为了保留代码原貌,有些地方不是很可读):
1 |
|
数据生成器(可以生成单个字符,并进行测试,注意需要调整被测试代码直接输出匹配字符串):
1 |
|