如果您要开发用于解决魔方的软件,您会如何表示魔方?
如果您要开发用于解决魔方的软件,您会如何表示魔方?
这篇ACM论文介绍了几种用于表示魔方的替代方法,并将它们进行了比较。不幸的是,我没有帐户获取全文,但描述中提到:
介绍并比较了7种不同的魔方表示法:3x3x3的3位整数数组;6x3x3文字数组;5x12文字矩阵;llxll稀疏文字矩阵;54个元素的向量;4维度数组;3x3x3嵌套数组。给出了用于定位移动、旋转和几个有用的解决魔方问题的工具的APL函数。
此外,这个RubiksCube.java文件包含一个相当清晰的表示以及相关的旋转部分的代码(如果您正在寻找实际代码)。它使用了一个单元格和面数组。
简而言之,这取决于您将如何解决魔方。如果您的求解器将使用人类方法(如逐层法或Fridrich法),则底层数据结构不会有太大的影响。即使在最慢的编程语言中,计算机也可以在可忽略的时间(不到一秒钟)内使用人类方法解决魔方。但是,如果您要使用更加计算密集的方法来解决魔方,例如Thistlethwaite的52步算法、Reid的29步算法或Korf的20步算法,则数据结构和编程语言至关重要。
我编写了一个魔方程序,它使用OpenGL渲染魔方,并内置了两种不同类型的求解器(Thistlethwaite和Korf)。求解器必须生成数十亿个移动并比较每个魔方状态数十亿次,因此底层数据结构必须快速。我尝试了以下几种结构:
在上述方法(3)的基础上,魔方的每个面由9个贴纸组成,但中心是固定的,因此只需要存储8个贴纸。 有6种颜色,因此每种颜色都适合一个字节。 给定这些颜色定义:
enum class COLOR : uchar {WHITE, GREEN, RED, BLUE, ORANGE, YELLOW};
一个人脸可能长成这个样子,存储在单个64位整数中:
00000000 00000001 00000010 00000011 00000100 00000101 00000000 00000001
被解码为:
WGR
G B
WYO
使用这种结构的优点是可以使用rolq
和rorq
按位运算符来移动一个面。向左或向右滚动16位将影响90度旋转; 向左或向右滚动32位将产生180度翻转。相邻的块需要手动维护-即在旋转顶部时,还需要移动前面、左边、后面和右边面的顶层。以这种方式旋转面非常快速。例如,滚动
00000000 00000001 00000010 00000011 00000100 00000101 00000000 00000001
通过16位生成
00000000 00000001 00000000 00000001 00000010 00000011 00000100 00000101
解码后,它看起来像这样:
WGW
Y G
OBR
另一个优点是,有些情况下可以使用一些巧妙的位掩码和标准整数比较来比较魔方状态。对于求解器来说,这可能会加速很多。
无论如何,我的实现在Github上:https://github.com/benbotto/rubiks-cube-cracker/tree/2.2.0,请参阅Model/RubiksCubeModel.{h,cpp}
。
延伸方法(4),编程解决魔方的一些算法使用了迭代加深的深度优先搜索和A*算法,并使用图案数据库作为启发式。例如,Korf算法使用三个模式数据库:一个存储8个角块的索引和方向;一个存储12个边缘块中的6个的索引和方向;最后一个存储另外6个边缘块的索引和方向。使用模式数据库时,一种快速的方法是将魔方存储为一组索引和方向。
任意定义一个约定,可以将边缘块按以下方式进行索引。
0 1 2 3 4 5 6 7 8 9 10 11 // Index.
UB UR UF UL FR FL BL BR DF DL DB DR // Position (up-back, ..., down-right).
RY RG RW RB WG WB YB YG OW OB OY OG // Colors (red-yellow, ..., orange-green).
因此,红黄边块位于索引0处,白绿边块位于索引4处。同样,角块可能被编号如下:
0 1 2 3 4 5 6 7
ULB URB URF ULF DLF DLB DRB DRF
RBY RGY RGW RBW OBW OBY OGY OGW
所以,红蓝黄角块的索引为0,橙绿黄角块的索引为6。
每个魔方块的方位也需要保持。边块可以有两种方向(定向或翻转),而角块则可以有三种不同的方向(定向、旋转一次或旋转两次)。有关块朝向的更多详细信息可以在这里找到:http://cube.rider.biz/zz.php?p=eoline#eo_detection。使用这个模型,旋转一个面意味着更新索引和方向。这种表示法最困难,因为对于人类来说(至少对于我来说)查看一大堆的索引和方向数字并验证它们的正确性很难。尽管如此,与使用上述其他模型动态计算索引和方向相比,这种模型显着更快,因此在使用模式数据库时是最佳选择。您可以在此处查看此模型的实现:https://github.com/benbotto/rubiks-cube-cracker/tree/4.0.0/Model (请参见 RubiksCubeIndexModel.{h,cpp}
)。
如前所述,该程序还呈现了魔方。我为此部分使用了不同的结构。我定义了一个“cubie”类,它由六个正方形组成,中心块、边块和角块的颜色面数分别为1、2或3。魔方由26个cubies组成。面是使用四元数旋转的。 cubies和cube的代码在这里:https://github.com/benbotto/rubiks-cube-cracker/tree/4.0.0/Model/WorldObject
如果您对我的魔方求解程序感兴趣,则可以在YouTube上找到一个高级概述视频:https://www.youtube.com/watch?v=ZtlMkzix7Bw&feature=youtu.be。我还在Medium上撰写了更详细的关于编程解决魔方的文章。
一种方法是着重于视觉外观。
一个立方体有六个面,每个面都是一个三乘三的正方形阵列。所以
Color[][][] rubik = new Color[6][3][3];
然后每个移动都是一种方法,可以置换特定组合的彩色方块。
new_cube[face][i][j] = clockwise ? old_cube[j][2 - i] : old_cube[2 - j][i]; }
- azz避免优化;使其面向对象。 我使用的伪代码类概述是:
class Square
+ name : string
+ accronym : string
class Row
+ left_square : square
+ center_square : square
+ right_square : square
class Face
+ top_row : list of 3 square
+ center_row : list of 3 square
+ bottom_row : list of 3 square
+ rotate(counter_clockwise : boolean) : nothing
class Cube
+ back_face : face
+ left_face : face
+ top_face : face
+ right_face : face
+ front_face : face
+ bottom_face : face
- rotate_face(cube_face : face, counter_clockwise : boolean) : nothing
使用的内存量非常小,处理也非常少,因此优化是完全不必要的,特别是当你为此牺牲代码的可用性时。
一个有趣的方法来表示魔方,被软件“魔方探索者”所使用。该方法使用许多巧妙的数学,只需使用5个整数就可以表示魔方。作者在他的网站上解释了程序背后的数学。据作者称,该表示适用于实现快速求解器。
你可以把魔方想象成三个垂直的循环链表,它们和三个水平的链表相交。
每当魔方的某一行进行旋转,你只需旋转相应的指针即可。
看起来就像这样:
struct cubeLinkedListNode {
cubedLinkedListNode* nextVertical;
cubedLinkedListNode* lastVertical;
cubedLinkedListNode* nextHorizontal;
cubedLinkedListNode* lastHorizontal;
enum color;
}
E
移动,则可以执行等效的 U D'
一对移动。这将减小大小到100位(13字节)。8!= 40320
种排列,而40320
可以用16
位来表示。3^7 = 2187
,可以用12
位来表示。12!= 479001600
种排列,而479001600
可以用29
位来表示。11
位。这总共需要68位(9字节)。
一个可还原魔方的最大置换数量是(8!*3^8*12!*2^12)/12 = 43,252,003,274,489,856,000 ~= 4.3*10^19
,可以存储在66
位(9字节)中,虽然可以枚举所有可能的解决方案,但没有必要保存这最后的2位。
const byte // symmetry
M[] = {2,4,3,5},
I[] = {2,0,4,6};
byte cube[55]; // 0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1, ... need to be filled first
#define m9(f, m) (m6(f, m)*9)
byte m6(byte f, byte m) {return ((f&~1)+M[m+(f&1)*(3-2*m)])%6;}
void swap(byte a, byte b, byte n) {
while (n--) {byte t=cube[a+n]; cube[a+n]=cube[b+n]; cube[b+n]=t;}
}
void rotate(byte f, byte a) { // where f is face, and a is number of 90 degree turns
int c=m9(f, 3), i;
swap(c, c+8, 1);
while (a--%4) for (i=2; i>=0; --i)
swap(m9(f, i) + I[i], m9(f, i+1) + I[i+1], 3),
swap(f*9+i*2, f*9+i*2+2, 2);
swap(c, c+8, 1);
}