![63f7bc71ba9013b470637400a3d1a9bd.png](https://i-blog.csdnimg.cn/blog_migrate/1a5ee0163a88928647ffdfd6eb0df2d1.jpeg)
魔方是个结构简单而变化无穷的神奇玩具。那么如何在万能的浏览器里模拟出魔方的无尽变换,又如何将其还原呢?下面让我们一步步地来一探究竟吧。
魔方的抽象
拆解过魔方的同学可能知道,现实中魔方的内部结构包含了中轴、弹簧、螺丝等机械装置。但当我们只是想要「模拟」它的时候,我们只需抓住它最显著的性质即可——3x3x3 的一组立方体:
![9565005c8e98967a3c91476d0cb115db.gif](https://i-blog.csdnimg.cn/blog_migrate/9ebd60c54a74bc0a5b7e6a827fa9963a.gif)
基本概念
上图演示了魔方最基本的思维模型。但光有这样的感性认识还不够:组成魔方的每个块并非随意安置,它们之间有着细微的区别:
- 位于魔方各角的块称为角块,每个角块均具有 3 个颜色。一个立方体有 8 个角,故而一个魔方也具有 8 个角块。
- 位于魔方各棱上的块称为棱块,每个棱块均具有 2 个颜色。一个立方体有 12 条棱,故而一个魔方也具有 12 个棱块。
- 位于魔方各面中心的块称为中心块,每个中心块仅有 1 个颜色。一个立方体有 6 个面,故而一个魔方也具有 6 个中心块。
- 位于整个魔方中心的块没有颜色,在渲染和还原的过程中也不起到什么实际的用处,我们可以忽略这个块。
将以上四种块的数量相加,正好是 3^3 = 27 块。对这些块,你所能使用的唯一操作(或者说变换)方式,就是在不同面上的旋转。那么,我们该如何标识出一次旋转操作呢?
设想你的手里「端正地」拿着一个魔方,我们将此时面对你的那一面定义为 Front,背对的一面定义为 Back。类似地,我们有了 Left / Right / Upper / Down 来标识其余各面。当你旋转某一面时,我们用这一面的简写(F / B / L / R / U / D)来标识在这一面上的一次顺时针 90 度旋转。对于一次逆时针的旋转,我们则用 F' / U' 这样带 ' 的记号来表达。如果你旋转了 180 度,那么可以用形如 R2 / U2 的方式表示。如下图的 5 次操作,如果我们约定蓝色一面为 Front,其旋转序列就是 F' R' L' B' F':
![ab96b6bb00998959cc19f9bb2845624f.gif](https://i-blog.csdnimg.cn/blog_migrate/807e4896233f1f609a0a22bffde6ebda.gif)
关于魔方的基础结构和变换方式,知道这些就足够了。下面我们需要考虑这个问题:如何设计一个数据结构来保存的魔方状态,并使用编程语言来实现某个旋转变换呢?
数据结构
喜欢基于「面向对象」抽象的同学可能很快就能想到,我们可以为每个块设计一个 Block 基类,然后用形如 CornerBlock 和 EdgeBlock 的类来抽象棱块和角块,在每个角块实例中还可以保存这个角块到它相邻三个棱块的引用……这样一个魔方的 Cube 对象只需持有对中心块的引用,就可以基于各块实例的邻接属性保存整个魔方了。
上面这种实现很类似于链表,它可以 O(1) 地实现「给定某个块,查找其邻接块」的操作,但不难发现,它需要 O(N) 的复杂度来实现形如「某个位置的块在哪里」这样的查找操作,基于它的旋转操作也并不十分符合直觉。相对地,另一种显得「过于暴力」的方式反而相当实用:直接开辟一个长度为 27 的数组,在其中存储每一块的颜色信息即可。
为什么可以这样呢?我们知道,数组在基于下标访问时,具有 O(1) 的时间复杂度。而如果我们在一个三维坐标系中定位魔方的每一个块,那么每个块的空间坐标都可以唯一地映射到数组的下标上。更进一步地,我们可以令 x, y, z 分别取 -1, 0, 1 这三个值来表达一个块在其方向上可能的位置,这时,例如前面所定义的一次 U 旋转,刚好就是对所有 y 轴坐标值为 1 的块的旋转。这个良好的性质很有利于实现对魔方的变换操作。
旋转变换
在约定好数据结构之后,我们如何实现对魔方的一次旋转变换呢?可能有些同学会直接将这个操作与三维空间中的四阶变换矩阵联系起来。但只要注意到一次旋转的角度都是 90 度的整数倍,我们可以利用数学性质极大地简化这一操作:
在旋转 90 度时,旋转面上每个角块都旋转到了该面上的「下一个」角块的位置上,棱块也是这样。故而,我们只需要循环交替地在每个块的「下一个」位置赋值,就能轻松地将块「移动」到其新位置上。但这还不够:每个新位置上的块,还需要对其自身六个面的颜色做一次「自旋」,才能将它的朝向指向正确的位置。这也是一次交替的赋值操作。从而,一次三维空间绕某个面中心的旋转操作,就被我们分解为了一次平移操作和一次绕各块中心的旋转操作。只需要 30 余行代码,我们就能实现这一魔方最核心的变换机制:
rotate (center, clockwise = true) { const axis = center.indexOf(1) + center.indexOf(-1) + 1