虎符签到题handle,连官方wp都叫你直接随便选一个看起来过得去的就行,确实是handle里对于开头的成语基本没啥讲究,但是反正做都做了不如做完做好,于是就又花了些时间跟着3b1b的wordle视频做了个这,主要是信息论的应用和对C++的顺便实操

HandleCrack[信息论]

为了文章长度尽量短点(懒),以下是我略过但读者应该知道的内容:

  • handle和wordle的规则
  • E = ∑ -p_i * log(p_i)

(直接去看一遍3b1b视频多好,以及另一个3b1b的补充说明

Q&A

>> 问:原题不是py吗怎么要扯cpp?

答:不会优化py所以我的cpp跑起来应该比py快(?),而且cpp的位操作和手动内存管理我看着要放心一点(执念(不要学我(呜

>> 问:不是随便选一个当开头词就可以了吗,为什么还要费这么大力气整这些?

答:一,看了wordle的视频自己也想实操一下;二,反正比赛都结束了不如做完善点;三,不求出来我会睡不着觉的。总之就是虽然确实根据最后的结果看来,只要选一个比较正常的成语,或者多试几个开头词,就能达到很好的效果了,但是我就是想做一下,因为挺有趣的

题面

Handle在4轮内找到答案,重复512次,每次的选词由random.choice()产生

然后就是他这个交互有点难受,由于你输入词语后它返回的拼音对错信息是以颜色标注的,虽然听群里说字节流下颜色可以比较方便的获取,但是反正后面我复现的时候是没有管这个的

下载链接(github源)

Handle杂谈

wordle是固定5个字母,每个字母框有三种情况,所以理论上总共的可能性是3^5种

handle是固定4个拼音,每个字的拼音分为声母,韵母,声调,所以可以看作是12个字母的wordle,也是每个拼音框有三种情况,理论可能性有3^12种

特殊韵母

但是中文中包含特殊韵母,比如 en,er,ei 等,一个韵母就是一个注音的字。那比如这一轮猜了读音为 en 的一个字,则不管才没猜对,都损失了一个位置上声母的信息,特别是 en,ei 这些可以和声母搭配,又可以自己来的特殊韵母,对于 er 而言,由于 er 不能和声母组合,会更特殊一点(顺便一提,py的拼音库得出的拼音不知为何对“了”这个字也没有音调,所以有些时候不止会在声母上为空,声调也有可能为空)

但是我已经不想思考了,所以我就把 er 和 en,ei 之类的同等处理的,也许把 er 再单独分出来又会稍微不一样一捏捏,但是我真的已经停止思考了

词库

其实我当时以为他题目没给代码,查了一下常用成语貌似也就几千个,我就去百度汉语扒了一个百度的成语词库,那也才4000个词啊,好家伙结果他给的那个列表有将近两万六千个词(?),看了一下是从这里面搬的,里面甚至有“可口可乐”之类的特别奇怪的东西,哈哈 (^ ^)

exp

这个exp里不包含对上面提到的特殊韵母的特殊处理,同时直接用的函数return的信息来反馈的,而不是题目环境下的颜色信息

吐槽

exp的思路很简单,的确只需要比较随便的选择开局的词语就可以了,甚至第一个词“一丁不识”在最后算出来的信息熵排名里面都很靠前,只要不是“可口可乐”这种奇怪的词,就可以在运气的加持下直接出flag,说实话这个flag是必须要运气的,因为就算是我最后算出来的信息熵最高的词他也不能保证4轮出结果,我得到的平均轮次大概是2.62轮(和题目条件一样随机抽取测试512次),而被我目击到的最大轮次是6轮,虽然我写的这个脚本优化比较烂,但是还是不影响我说:这个题他应该看你到最后的平均轮次是否合格,而不是看最大轮次 = =

在的官方wp里对他自己的exp表示:

这个脚本也不是必成功,但是体感成功率非常高,平均猜词次数 2.72,也许换个起始词可以把最大次数压到 3

哥你得有数据啊不要凭感觉啊,出题人可能是觉得他随便选了个词就可以把平均猜词数压倒2.72,所以优化一下应该能轻松压得很低,但是问题就是其实这些词语的信息熵的分布总体来看都算是比较高的了,总共不到2.6w个词,2.1w个词的信息熵在10以上,最高的信息熵也就12.8755,意思是你随便选中那2.1w个信息熵在10以上的词的时候,理论上剩下的词数的期望就只有25.3个,但3b1b讲wordle的视频里有句话说得好,就,“期望是期望,实际是实际”(大概是这个意思),所以还是那句话,最好就存一下选手的轮次信息,最后算一下平均轮次够不够小

在我抽的512抽里面大概是这么个分布

1
2
3
猜中轮次 -| 一发入魂 |    2    |    3    |    4    |    5    |    6    |
-----------------------------------------------------------------------
发生次数 -| 1 | 230 | 247 | 32 | 1 | 1 |

Source

挺无脑的,py水平有限,有些地方不太优雅

最优开头

我其实之前想过能不能直接构造一个最优开头,但是没什么思路,就决定从他给出的2.6w个词里面找一个最优开头

由于文章一开头说的优化问题,所以我就py2cpp()

准备工作

首先我把所有词的拼音都通过py的拼音库导出到了硬盘,无韵母无声调的地方用“#”进行了占位,然后为了之后不再重复统计一个词另一个词比较得出的pattern,我把所有partern导出,这样之后查表就完事了

这一块值得注意的应该也就是pattern的存储格式

因为每一个拼音位有4种情况:确定存在且位置正确,确定存在但位置错误,确定不存在,不确定是否存在(特殊韵母);而总共4个汉字,每个汉字3个拼音位,由于状态共4种,两位就能表示完(00,01,10,11),总共需要4 * 3 * 2 = 24(bit),刚好3字节,所以用3个char就可以塞下1个pattern,这部分详见下方折叠代码中的CalPattern()

main

做好了patterns的准备工作,就可以开始求E了

对于GetEntropy()总共就三步:

  • 根据最大E猜词
  • 求新的E
  • 更新词库并回到第一步

存储结构上要提一下,如果直接创建超大数组多半会segFault,所以我用的拉链法

1
char patterns[MAXSIZE][MAXSIZE * 3]; // => Segment Fault
1
2
3
4
5
6
char *patternsHead[MAXSIZE];
for (int i = 0; i < MAXSIZE; i++)
{
char *patterns = new char[3 * MAXSIZE];
patternsHead[i] = patterns;
} // => 彳亍

以及递归深度方面,由于深度只要不为0就算的真的太慢了,所以我也和3b1b一样,计算深度为1的数据时只拿在深度为0时排名靠前的数据去计算,以减少对获取最优开头词的无效计算

其他

其实我是准备吧handle完整搬到c++上来的,结果代码弄完了发现我不知道怎么让c++支持中文输入(?

但是不准备继续日了,放个之前的代码

还有就是这令人发指的运行速度,不会多核多线程优化所以没办法

学了OS后的补充:现在会了

但是可以加入计时函数量化我的痛苦 :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <time.h>
double CheckTime(clock_t &start, clock_t end)
{
double delta = double(end - start) / CLOCKS_PER_SEC;
start = end;
return delta;
}
int main()
{
clock_t clockStart;
clockStart = clock();
// ... sth.
cout << "time length: " << CheckTime(clockStart, clock()) << endl;
}

windows下g++编译,depth=0跑完16个大约是12sdepth=1跑完4个大约是32s,我大概是花了4~5h跑完所有数据

以及千万不要让测速度的脚本能覆盖结果,像我直接把之前算完的给覆盖了,全部重来

运行结果

经过排序发现

研经铸史


是handle的最优开头,且在depth=1的情况下E=14.8701,以ΔE=0.03的巨大差值(笑)和第二名拉开距离

结果统计

基本全部正常的词在d0的E都在10以上,少数如“可口可乐”,d0E=6.55192,这样的词拉低了整体下限,整体d0E分布如下

对于前d0E排名top100的词进行d1E的计算和排序,分布如下

日不动了,再见 UwU

Comments

⬆︎TOP