魔方吧 关注:507,274贴子:11,811,292

魔方还原算法(二)科先巴的二阶段算法

只看楼主收藏回复

本文来具体介绍一种具体的魔方还原算法——科先巴的二阶段算法,有一部分相关内容在前篇帖子讲述,主要是方向定义那一块儿,没有看的建议先看一下。也可以在我的公号中看原文,格式更好,更方便阅读。
二阶段,顾名思义,解决问题分为两步,先完成一个目标,再最终复原。对于二阶段算法有一个生动的比喻,复原魔方就像是一条小船要在汪洋大海上行驶到一个固定的目的地。二阶段算法就是先让小船行驶到一个固定的特殊水域,再驶向最终的目的地,这显然比直接寻找目的地要容易简单的多。
比喻归比喻,终归抽象,我们来具体看看,本篇很长很长,我分开来说。
一、总述
为方便叙述,也避免搞混淆,我们定义任意打乱的魔方称为随机状态或者初始状态,处于特殊水域的那些状态称为目标状态,目的地为还原状态。
初始状态可以看作是由 U,R,F,D,L,B 这 6 个基本转动复合而成的,由这 6 个转动生成的群记为 G=<U,R,F,D,L,B> 。
而科先巴定义的目标状态是只由 U,D,L2,R2,F2,B2 这 6 种转动复合生成的,同样的记为 G1=<U,D,L2,R2,F2,B2>。
Ⅰ 目标状态性质
还记得上篇文章计算状态数时是怎么定义方向的么?再来复习复习重新看看:
1. 棱边有 2 种状态,未翻转(flip)用 0 表示,翻转用 1 表示, 只有 F,B 层的转动能够改变方向。
2. 角块有 3 种状态,未扭转(twist)用 0 表示,顺时针扭转用 1 表示,逆时针扭转用 2 表示,只有 U,D 层转动不改变角块方向。
目标状态由 U,D,L2,R2,F2,B2 这 6 种转动生成,而这 6 种转动不会引起角块和棱块方向的变化。因为不论怎么转动,角块拥有的 U 层面或 D 层面始终处于 U 层或者 D 层,所以方向不变。也不会引起棱块方向变化,因为只有 F,B 的转动会引起棱块方向的变化,而像 F2 这样的连续转动两次,翻转再反转棱块相当于未翻转,所以棱块的方向也是不会变化的。这 6 种转动也不会将中层块带到 U 层或者 D 层,所以处于中层的块只能处于中层。
因此目标状态的性质总结如下:
1. 棱块角块方向不变,即各自的方向值总和都为 0。
2. 还原状态下的中层块处于中层
Ⅱ 搜索算法
科先巴的二阶段算法使用的搜索算法是 IDA* 算法,也就是迭代加深搜索算法,而且两个阶段使用的都是 IDA* 算法。
关于IDA*,还有个 A*算法大家可能不太熟悉,也没什么神秘的,简单说说。A* 就是具有启发函数的广搜。IDA*就是有搜索深度限制,有启发函数的深搜。点到为止,详细的解释请百度 CSDN。
一般的,广搜是能够找到最短路径的,深搜不能,IDA*虽然使用的是深搜,但是它有深度限制,也是能够找到最短路径。比如说我搜索深度定为 1 ,没搜到,再定为 2 重新搜索,依次下去,就能够得到最短路径。
所以两个阶段都是能够得到相应的最短路径,能够使用最少的步数达到每个阶段的目标,但合起来会是最优解吗?
答案是否定的,因为阶段二得到的最优解是相对于阶段一来说的,举个例子

两个阶段都相应最优就好比上述方法 1 得到的距离,但两个阶段合起来明显不是最短距离,两点之间线段最短应该是方法 4。
那怎么解决呢,如何得到更优甚至最优的解法?也如上图所示,我们加深第一阶段的搜索深度,虽然第一阶段的路径变长了,但是第二阶段可能找到更短路径,这样它们合起来边可能更优。这样一步步下去,是总能找到最优解的,也就是说科先巴的二阶段算法一次搜索可能得不到最优解,但是只要给它时间,是一定能够找到最优解的。但是对于科先巴算法并不是拿来寻找最优解的,目的是短时间内找到一组比较优的解。
对于科先巴的二阶段算法应该有了很直观的基本认识了吧,如果还不清楚,再针对魔友举一个很形象的例子。咱们一般使用的 CFOP 还原公式,F2L 之后就应该是 OLL,PLL 的对吧,如果我们正常的按照 CFOP 的公式来,这几个阶段都得到的是最优解,合起来的话也是比较优的。但是如果我们在 F2L 时多花几步,是可能跳 O 甚至跳 P 的,这就会节省很多后续转动步骤,得到一个更优的解法。
上述有关两点之间线段最短的例子只是比喻,实际情况稍微有点不一样,前面说过二阶段算法就是小船先驶向一片特殊水域,再驶向目的地,但是某些情况的最短路径可能根本就不经过那片水域,如果还是先经过特殊水域的话,自然也就得不到最优解。那这是不是就与上述所讲的冲突呢?也不然,还原状态其实也是目标状态之一是吧,所以如果解决阶段一之后发现已经还原了,阶段二所需要的步骤降为 0 ,那这就是最优解。就好比完成 F2L 之后惊奇的发现跳 O 又 条 P 已经还原了。
好了,上述就是对科先巴的二阶段算法的一个总述,应该是很容易理解的。下面是算法细节,挺多,感兴趣的朋友可以看看,对魔方没什么兴趣的了解到这一步大概就够了。
不过学计算机的,关于魔方的还原算法还是很值得一看的。不知道大家有没有做过八数码的题,解魔方与解八数码本质上是一样的,英文也都用 puzzle 这个词来表示,只不过魔方的状态数比八数码要大得多,而且魔方方方正正的,有许多特殊的性质可以利用来优化算法,下面来具体看看:


IP属地:北京1楼2021-09-20 19:54回复
    感觉东西


    IP属地:浙江来自iPhone客户端2楼2021-09-20 20:00
    回复
      啊这


      IP属地:浙江来自Android客户端3楼2021-09-20 20:09
      回复


        IP属地:广东来自Android客户端4楼2021-09-20 20:13
        收起回复
          好 快发


          IP属地:新疆来自Android客户端6楼2021-09-20 20:54
          收起回复
            唉,贴吧这文字格式有点醉人,据贴友说好像要签到一个月才有文字编辑功能,对新人不太友好,就先这样吧,有兴趣的可以去公号里去看原文。
            重发一次,上次的组合数那儿有点格式问题,没显现出来。
            接着上文:
            二、定义魔方
            定义魔方从两个层次来定义,一个是块层次,一个是坐标层次
            定义魔方,首先就得对角块,棱块这些基本元素标号,可按如下方式这样标号:

            代码表示如下:
            typedef enum {URF,UFL,ULB,UBR,DFR,DLF,DBL,DRB} Corner;
            typedef enum {UR,UF,UL,UB,DR,DF,DL,DB,FR,FL,BL,BR} Edge;
            对角边进行标号了之后就可以从块这个角度来定义一个魔方:

            怎么存储的魔方状态?举个例子 co[URF] = {URF, 0}表示 URF 这个位置存储的 URF 这个块,方向为 0。co[UFL] = {UFL, 1} 表示 URF 这个位置存储的 UFL 这个块,方向为0 。
            棱块同理,而在篇一就说过,我们不用考虑中心块,中心块保持不动,当做参考系。如此就可以用上述的 CubieCube 结构体来表示一个魔方状态,定义一个魔方。
            Ⅱ 坐标层次(索引)
            这名字是从英文名中直译过来的,这一层次简单来说就是我们要对魔方状态进行编码/哈希,给不同状态分配一个唯一数来标识,主要用来作为变换表,剪枝表的索引。当然不可能真的对魔方不同的完整状态进行编码,这状态数太多了,不现实。而是对魔方的部分状态编码,一个魔方的完整状态有 8 个角块,12 个棱块以及它们的方向组成,而算法就是将它们分割,比如某些棱块的位置,角块位置,角块方向,棱块方向等等,对它们进行编码哈希,这样数量就会小上很多。
            因此主要是对于位置,方向两个指标编码,而编码方法主要用到以下三种:
            ① 方向
            方向分为角块方向和棱块的方向,两者类似,在这儿以角块方向举例,假如有如下所示的一个带方向的角块排列:

            第一行表示位置,第二行表示该位置上存储的角块和其方向,使用下面的方法进行编码:

            应该很直观也很好理解吧,因为当 7 个角块方向确定之后,最后一个角块的方向也就确定了,所以不用编码最后一个角块的方向。代码如下:

            ② 排列
            对于位置变化常使用排列数,期间我们可能会用到多种排列,比如 8 个角块的排列,6 条边的排列等等,这些都是魔方部分状态的表示,我们需要进行编码,有关排列的编码方法有个名字叫做康托展开,同样直接来看一个例子来说明康托展开这种编码方式,假如 8 个角块有如下排列:

            第一行表示位置,第二行表示对应位置上放的角块,由标号知道咱们的块是有大小之分的:URF<UFL<ULB<UBR<DFR<DLF<DBL<DRB 所以第三行就表示位于左边的块比自己大的块有几个,比如说 URF 块(看第二行,第一行表示位置,第二行才是该位置上存放的块)左边有 3 个块,都比自己大,所以第三行值为 3;再比如 UBR 块,左边有 7 个块,比自己大的有 4 个,所以值为 4 。DFR 块处于第一个位置,左边没有块了,所以编码时第一个块不用考虑进去。
            具体的编码:

            这个式子与上面第三行一一对应起来应该很好理解就不解释了,代码描述如下:

            ③ 组合
            对于位置变化有时也会用到组合数,需要对组合数进行编码,在阶段一我们会用到关于中间层 4 个棱块的一个组合数编码,阶段一的目标状态要求本来在中间层的棱块要处在中间层,所以算阶段一的棱块位置状态我们只用考虑中间层的 4 个棱块,有 12*11*10*9 = 11880 种状态,但是中间层的 4 个棱块之间并无位置要求,所以还要除以它们自身的位置排列 4!= 24 ,所以总共 495 种状态,而这就是一个组合数 Choose(12,4 ,我们要对这 495 种状态编码:

            第一行表示位置,第二行表示存储的块,只用考虑中间层 4 个块的位置也就是 FR,FL,BL,BR 4 个棱块, 因为中间层的 4 个棱块无位置要求,所以用 X 代替,表明它们是相同的。第三行表示 X 对应的组合数是多少,以第二个举例说明怎么编码:

            直接用到了组合数的值,所以要提前写一个求组合数的函数,n 选 k,如果 k>n 规定等于 0,写函数时判断以下即可。上述编码也可以对另外的 8 个组合数和来表示,感兴趣的可以自己试试。
            上述是编码过程,魔方的坐标层次表示,也就是将魔方的部分状态转变成一个特定的数来表示,编码方式不唯一,合理即可。我们还应需要一个逆过程,从编码到实际的魔方状态,这样才能实现上述两种层次的定义相互转化,这里不再赘述了,基本上就是完全反着来一遍。
            上述就是定义魔方,从块层次和坐标层次来定义,特别是坐标层次,这在整个程序中至关重要。由于只能插入 10 张图片,又没法打公式,就只好一些图片和文字放在一起截图了,还请见谅。


            IP属地:北京7楼2021-09-20 21:01
            回复
              三、定义转动
              前面定义了魔方本身,这里来定义魔方操作,也就是定义转动
              Ⅰ 位置变化
              先来看篇一提到的 {0,1,2,3} 的排列问题,假如有两个排列,在程序中用数组如下表示:
              int a[4] = {0, 1, 2, 3};
              int b[4] = {1, 2, 3, 0};
              int c[4] = {};
              数组这个数据结构有两要素,一个是下标表示位置,一个是元素,都是同等重要的,但往往我们可能会忽略掉下标的作用。
              b 这个数组表示什么?当然可以表示一个排列,0号位存的 1,1号位存的2 ,2号位存的3,3号位存的0。
              这只是常规认识,还表示什么?本身就可以表示一个变换,它表示 0 号位的元素被 1 号位元素替代,1 号位的元素被 2 号位元素替代,2 号位的元素被 3 号位元素替代,3 号位的元素被 0 号位元素替代。使用这种替代的角度来描述变换有很直观的等式关系,比如,0 号位的元素被 1 号位的元素替代,可以写成 a[0] = a[1] 。
              所以给定一个排列,它不仅是排列,还可以看作是一个变换,这是因为排列本身就存在次序,从数组的角度来就是有个隐含的下标来表示元素所处的位置,通过一个元素前后次序的变化就可以说明一个变换操作。
              特别的如果元素个数也就是下标取值范围等于元素的取值范围,我们就可以很方便的在其上定义封闭的变换。比如说上述的排列元素个数为 4,所以下标的取值范围为 0~3,而元素本身的取值也是 0~3 这 4 个数。这种情况就很方便定义变化操作,比如在 a 这个排列上做 b变换,得到的结果存在排列 c 里面,进行如下操作:
              for(int i = 0; i < 4; i++)
              c[i] = a[b[i]];
              总结下来就是如果给定一个排列,如果要把它当作是一个变换的话,那么不仅下标表示的是位置,它的元素也表示的是位置。如 b[2] = 3就表示位置 2 的元素被位置 3 的元素替代。
              理解了上面之后,魔方转动的定义也就迎刃而解了,并不需要什么新的数据结构,前一节定义的魔方状态就可以一个变换,只不过在不同需求时我们对其解读不同罢了。
              Ⅱ 方向变化
              一个位置存储的不仅有块本身,还有块的方向,块可以按照上述替代的角度来看,但方向不行。
              举个例子,在 a 状态下做 F 操作,前面说过转动操作也可以看作一个状态,我们把 F 操作记为 b ,得到的结果记为 ab,以 URF 这个位置为例来说明。
              F 转动使得 URF 槽的元素被 UFL 槽的元素替代,可以写成

              但是方向的取值不能用替代来表示,也就是说 ab.co[URF].o != a.co[UFL].o,F 转动下,UFL 位置上的元素移动到 URF 位置上,并顺时针扭转,所以对于 F 转动,方向应有以下变化:

              Ⅲ 基本转动
              因此我们应该能很容易的写出基本转动的定义,来看个例子 F :

              Ⅳ 转动函数
              转动分为角块转动和棱块转动,所以代码如下:

              这个函数就是角块转动和棱块转动函数的封装,以角块的转动函数为例说明:

              与科先巴的源程序不太一样,原程序还处理了镜像等情况,这里先不管,后面再说。上面讲述的角度是将 a 看作魔方的一个状态,b 看作是一个变换,但同样也可以将 a 看作是一个变换,那么 ab 就相当于一个复合操作,一样的道理,只是理解角度不同而已。
              Ⅴ 转动表
              转动在程序里面会经常用,每次都去做一遍变换操作太费时间,我们要建立转动表,只要在第一次运行程序时建立好这个表,以后的转动操作直接去查表就行了。
              转动表有很多,取决于使用了哪些坐标来表示魔方状态,以角块方向为例:

              这里就用上了魔方状态定义的坐标层次,把它当作索引。举个例子说明上面二维数组的意思,假如有twistMove[a][U] = b ,说明状态 a 下,进行 U 操作,得到状态 b。a, b 是两个数,分别唯一标识着魔方的角块方向状态。
              这个二维数组初始化也很容易,直接看码:

              这部分是定义转动和转动表,就是将魔方的基本操作用程序语言给表示出来,就像是数学中的矩阵变换一样,只是这里使用了程序语言写了出来,前面这些应该还是很好理解的,后面讲述对称


              IP属地:北京8楼2021-09-20 21:22
              回复
                看不懂,膜


                IP属地:天津来自iPhone客户端9楼2021-09-20 21:36
                收起回复
                  nb


                  IP属地:陕西来自Android客户端10楼2021-09-20 21:38
                  回复
                    楼主提到的科先巴二阶段算法应该和domino reduction是一样的。二阶段又可以称为降群,先将魔方从完全无序的群降为有一定规律的群,从而减少状态数,具体也有很多思路。这在最少步上有很多应用。我找到一个教程可以参考https://www.bilibili.com/read/mobile?id=5635261
                    还有很多研究思路,例如zz法和盲拧四步法都对棱块色相有定义,盲拧彳亍法以及高阶盲拧用到了转换机的理论,最少步除了降群以外还有筑块、逆序、插入等思路。有兴趣可以看看相关资料


                    IP属地:江苏来自Android客户端13楼2021-09-20 21:57
                    收起回复


                      星座王
                      点亮12星座印记,去领取
                      活动截止:2100-01-01
                      去徽章馆》
                      IP属地:山东来自Android客户端14楼2021-09-20 22:24
                      回复


                        IP属地:北京来自iPhone客户端15楼2021-09-20 22:52
                        回复


                          IP属地:重庆来自Android客户端16楼2021-09-21 07:47
                          回复
                            看起来很牛逼的样子


                            IP属地:辽宁来自Android客户端17楼2021-09-21 08:36
                            回复
                              一大段文字搁着放阅读题呢不过楼主真的🐮


                              IP属地:广东来自Android客户端18楼2021-09-21 12:08
                              收起回复