游戏玩法
开局任选一个空格翻开,作为起始点;
如果紧挨着该棋子且在同一条直线上有连续两个空格,则可以移除该棋子,把另外两个空格翻开(操作就是点一下远端的空格,再点一下该棋子,就会自动实现上述操作);
- 重复上述操作,直到只剩一个空格为止。
试玩体验
既然被称为智商检测器,对于普通玩家来说极具挑战性。打开游戏 20 分钟后,尽管已经重新开始了无数次,但依然毫无头绪。
在没有思路的情况下,即便通过大量尝试碰巧赢了游戏,也不会有任何成就感,反而会在侥幸中感到空虚和迷茫。因此我决定写个程序暴力算出攻略,反正也好久没写代码了,就当练练手。
于是新建了一个 Python 文件,准备暴力求解。
实现
其实这个任务最开始给我的感觉是银行家算法,每次移动前提前判断这样做之后是否存在能完成游戏的安全序列。但我仔细想想其实跟银行家算法的侧重点并不同,因为我们只要能找到一个安全序列就能完成游戏,而整个问题的核心也恰恰在于找到这个安全序列。
而从实现的角度而言,跳房子游戏的算法应该比银行家算法稍微复杂一些。如果把棋子占用格子和释放格子类比成进程占有资源和释放资源,那么棋盘中的棋子实际上是一种'常驻'的进程,这又会对后续的分配造成影响。所以直接套用银行家算法是不太合适的。
不过好在游戏规则简单,问题也没有到很复杂的程度。那么我们开始吧。
棋盘建模
首先,我们如何表示棋盘呢?
我们希望有一种模型能够体现这些空格或者棋子的相对位置关系,该模型应该包含空格的编号和属性。对于编号而言,我选择直接用整数编码,用 0~14 的整数来标识每一个空格,这样也对应列表的下标,较为方便。
但属性就值得思考了。根据游戏规则,在这些棋子的所有相对位置关系中,用到的就只有是否相邻、是否共线这两个。
我希望能给这些点设计一个属性,当我任取三个点,都能方便地判断他们是否满足相邻且共线的关系。比如,给定 (0, 1, 3),返回结果 True,给定 (0, 1, 2),返回结果 False。
坐标系
我首先想到的是坐标,这也是最符合直觉的。把这个棋盘映射到平面直角坐标系中,然后建立解析几何关系,用来判断点与点之间的联系。共线关系可以直接用三点中任意两点连线的斜率来判断,而相邻关系可以用两点之间的欧氏距离。
然而,如果不对原棋盘进行变形,那么这些点的坐标中就不可避免地会出现无理数,这是我不想看到的。将等边三角形便变形成一个等腰直角三角形可以解决这个问题,但这样的话,点与点之间的距离就会各不相同,进而引起其它麻烦。
这才是整个任务的第一步,我不想在这上面花太多精力。因此,我就没有再考虑坐标系方法了。
卡诺图
卡诺图是数字逻辑课程中学习的知识。既然我希望任意给出 3 个点,输出是或不是的判断,那不就可以通过逻辑表达式来实现吗?结合卡诺图可以对表达式进行化简并具象出来。
然而,我稍加回忆,又很快否决了这个方案。我们学习时的卡诺图是二进制的,这样即便有 8 个甚至更多维度,我们也能通过卡诺图进行表示、化简。然而,在这个任务中,尽管只有 3 个维度(即输入的 3 个点),但每个点都是 15 进制,这就意味着如果我们想要枚举所有情况,就必须画一个 15x15x15 的卡诺图,然后再化简。这显然是不现实的。
我数了一下,图中一共有 18 个三点共线相连关系,即便得到了逻辑关系,也将是一个极其冗长的表达式。
神经网络
放弃卡诺图之后,我脑海中突然又闪过了一个邪恶的念头。既然通过人力来提取逻辑表达式工作量太大,那我设计一个神经网络,让机器自己学习输入输出之间的逻辑关系,不就行了吗?
对于这样一个简单的分类任务,即便是最基本的多层感知机也绰绰有余。然而问题又来了,这样就得我自己构建一个数据集,给每一个可能的输入标注上分类结果。虽然只有 16 个正样本,但依然需要耽误一会儿时间,而且后面还要训练。
因此,神经网络方法也不合适。
位置嵌入(positional encoding)
我叫着玩儿的,借鉴了 Embedding 的概念。其实 Embedding 这个词用在深度学习中的应用也不是很准确,只不过大家都这样叫就约定俗成了。
既然上面一种方法已经需要我手动标注所有数据了,我何不顺势而为找一种相对简洁的方法来实现呢?最终,我选择了一种 18 位长的 01 序列来给每一个节点编码。如果我们只观察这 15 个节点的编码的第一位,则只有 0、1、3 三个节点的该位是 1,其它节点该位都是 0。以此类推,把 18 组关系全部放进编码中。这样,我们就可以直接通过位操作,把任意 3 个点的编码相与,如果结果不是全 0,就说明符合我们想要的关系。
15 个点的编码分别为:
NodesEmbedding = [
,
,
,
,
,
,
,
,
,
,
,
,
,
,
]


