提问者:小点点

为傻瓜解决魔方


杜姆先生:你好,我很笨,但我还是想解决一个3x3x3的魔方。

斯马特先生:嗯,你很幸运。这里有这样做的指导!

杜姆先生:不,那对我不起作用,因为我是杜姆。我只能遵循这样的算法。

pick up cube

look up a list of moves from some smart person

while(cube is not solved)
    perform the next move from list and turn
    the cube as instructed.  If there are no
    more turns in the list, I'll start from the
    beginning again.

hey look, it's solved!

斯马特先生:啊,没问题,这是你的单子!

好的,那么什么样的列表可以解决这样的问题呢?我知道魔方永远不会离要解决的20步更远,并且魔方有43,252,003,274,489,856,000个排列。因此,我认为这个列表可能是(20*43,252,003,274,489,856,000)长,但是

  • 有人知道目前已知的最短的列表吗?
  • 你如何找到理论上最短的列表?

请注意,这纯粹是一个理论问题,我实际上不想对计算机进行编程来做到这一点。


共2个答案

匿名用户

通过立方体的所有排列获得这样一条路径的想法是使用人类求解者使用的一些序列。Smart先生算法的主要结构如下所示:

function getMoves(callback):
    paritySwitchingSequences = getParitySwitchingSequences()
    cornerCycleSequences = getCornerCycleSequences()
    edgeCycleSequences = getEdgeCycleSequences()
    cornerRotationSequences = getCornerRotationSequences()
    edgeFlipSequences = getEdgeFlipSequences()
    foreach paritySeq in paritySwitchingSequences:
        if callback(paritySeq) return
        foreach cornerCycleSeq in cornerCycleSequences:
            if callback(cornerCycleSeq) return
            foreach edgeCycleSeq in edgeCycleSequences:
                if callback(edgeCycleSeq) return
                foreach cornerRotationSeq in cornerRotationSequences:
                    if callback(cornerRotationSeq) return
                    foreach edgeFLipSeq in edgeFlipSequences:
                        if callback(edgeFlipSeq) return

5个get…函数都将返回一个序列数组,其中每个序列都是一个移动数组。回调系统将避免将所有移动保存在内存中的需要,并且如果目标语言可用,可以用更现代的生成器语法重写。

哑巴先生会有这样的代码:

function performMoves(sequence):
    foreach move in sequence:
        cube.do(move)
        if cube.isSolved() then return true
    return false

getMoves(performMoves)

Dumb先生的代码将他的回调函数传递给Smart先生一次,然后Smart先生将继续回调该函数,直到它返回true。

Smart先生的代码将遍历5个get函数中的每一个,以检索他开始向调用者生成序列所需的基本序列。我将在下面描述这些函数,从其结果用于最内层循环的函数开始:

想象一个立方体,除了可以翻转但仍在右槽中的边缘之外,所有的部分都在它们的右槽中并正确旋转。如果它们被翻转,立方体就会被解决。因为有12条边,但边只能同时翻转2条,所以这个立方体的边缘翻转(或不翻转)的方式数量是2^11=2048。换句话说,12条边中有11条可以有任何翻转状态(翻转或不翻转),而最后一条边被另外11条的翻转绑定。

这个函数应该返回同样多的序列,这样在应用其中一个序列后,立方体的下一个状态就会产生,它具有一组唯一的翻转边缘。

function getEdgeFlipSequences
    sequences = []
    for i = 1 to 2^11:
        for edge = 1 to 11:
           if i % (2^edge) != 0 then break
        sequence = getEdgePairFlipSequence(edge, 12)
        sequences.push(sequence) 
    return sequences

内部循环确保在外部循环的每次迭代中进行一次翻转,您可以获得所有可能的翻转状态。

这就像用二进制表示列出所有数字,只需翻转一位即可到达下一个数字。以这种方式生成数字的输出将不会有序,但您会得到所有数字。例如,对于4位(而不是11位),它会像这样:

0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000

该序列将决定哪一条边与第12条边一起翻转。我现在不会去定义getEdgePairFlipSequence函数。很明显,有翻转任何一对边的序列,在它们不公开的地方,人们可以很容易地进行一些移动,将这两条边带到一个更好的位置,进行双重翻转,并通过以相反的顺序和相反的方向应用开始移动,将这些边再次返回到它们的原始位置。

想法和上面一样,但是现在有了旋转的角。不同的是一个角可以有三种旋转状态。但是就像翻转的边缘一样,如果你知道7个角的旋转(已经在它们的正确位置),第8个角的旋转也是确定的。所以立方体有3^7种可能的方式可以旋转它的角。

与第8个角一起旋转一个角的技巧,因此找到所有可能的角旋转也适用于这里。3基数表示中的模式如下(对于3个角):

000
001
002
012
011
010
020
021
022
122
121
120
110
111
112
102
101
100
200
201
202
212
211
210
220
221
222

所以这个函数的代码如下所示:

function getCornerRotationSequences
    sequences = []
    for i = 1 to 3^7:
        for corner = 1 to 7:
           if i % (3^edge) != 0 break
        sequence = getCornerPairRotationSequence(corner, 8)
        sequences.push(sequence)
    return sequences

同样,我不会定义getCornerPairRotationSequence。类似的推理适用于边缘。

当你想在不影响立方体其余部分的情况下移动边缘时,你需要循环至少3条,因为不改变其他任何东西就不可能交换两条边缘。

例如,可以交换两个边和两个角。但那超出了这个函数的范围。我稍后在处理最后一个函数时会回到这个问题。

该函数旨在找到所有可能的立方体状态,这些状态可以通过重复循环3条边来到达。有12条边,如果你知道其中10条的位置,剩下的2条的位置就确定了(仍然假设角保持在它们的位置)。所以在这些条件下有12! /2=239 500 800个可能的边排列。

就内存而言,这可能有点问题,因为要产生的序列数组将占用该数字的倍数(以字节为单位),因此我们可能会谈论几个GB。但我假设有足够的内存:

function getEdgeCycleSequences
    sequences = []
    cycles = getCyclesReachingAllPermutations([1,2,3,4,5,6,7,8,9,10,11,12])
    foreach cycle in cycles:
        sequence = getEdgeTripletCycleSequence(cycle[0], cycle[1], cycle[3])
        sequences.push(sequence)
    return sequences

函数将返回一个边的三元组的数组,这样,如果你将循环从左到右的边列在一个三元组中,并重复这个完整的数组,你会得到所有可能的排列边(不改变角的位置)。

我问的这个问题的几个答案可用于实现getCyclesReachingAllPermutations。基于此答案的伪代码可能如下所示:

function getCyclesReachingAllPermutations(n):
    c = [0] * n
    b = [0, 1, ... n]
    triplets = []

    while (true):
        triplet = [0]
        for (parity = 0; parity < 2; parity++):
            for (k = 1; k <= c[k]; k++):
                c[k] = 0
                if (k == n - 1):
                    return triplets
            c[k] = c[k] + 1
            triplet.add( b[k] )
            for (j = 1, k--; j < k; j++, k--):
                swap(b, j, k)
        triplets.add(triplet)

类似地,对于其他主要函数,这里也依赖于函数getEdgeTripletCycleSequence,我不会扩展它。有许多已知的序列可以循环三个边,用于几个位置,其他的可以很容易地从它们中导出。

我会保持简短,因为它和边是一样的。如果边不动,角有8个! /2可能的排列。

function getCornerCycleSequences
    sequences = []
    cycles = getCyclesReachingAllPermutations([1,2,3,4,5,6,7,8])
    foreach cycle in cycles:
        sequence = getCornerTripletCycleSequence(cycle[0], cycle[1], cycle[3])
        sequences.push(sequence)
    return sequences

这个额外的级别是为了处理立方体可以处于奇数或偶数位置的事实。当需要奇数个四分之一移动(然后半圈算作2)来求解立方体时,它是奇数。

我之前没有提到它,但是上面所有使用的序列都不应该改变立方体的奇偶校验。当我写置换边时,角应该保持在它们的原始位置时,我确实隐含地引用了它。这确保了奇偶校验不会改变。另一方面,如果您将应用一个同时交换两个边和两个角的序列,您必然会切换奇偶校验。

但是由于上面的四个功能没有考虑到这一点,所以需要这个额外的层。

功能很简单:

function getParitySwitchingSequences
    return = [
        [L], [-L]
    ]

L是一个常数,代表立方体左面的四分之一移动,-L是相同的移动,但相反。它可能是任何面。

切换立方体奇偶校验的最简单方法就是:执行四分之一移动。

这个解决方案当然不是最佳解决方案,但它最终会经历立方体的所有状态,尽管在此过程中会出现许多重复的状态。它将在两个连续排列之间进行少于20次的移动。移动次数将在1(奇偶校验切换)和18(翻转两条边允许2次额外移动以使一条边处于良好的相对位置)之间变化,并且在双翻转后将该边放回14次,我认为这是最坏的情况。

一个快速的优化方法是将奇偶循环作为内环,因为它只包含四分之一的移动,让那个重复最多会更有效。

已经构建了一个图,其中每条边代表一次移动,节点代表所有唯一的立方体状态。它是循环的,使得从最后一个节点向前的边将您带回第一个节点。

因此,这应该允许您通过尽可能多的移动来遍历所有立方体状态。显然不存在更好的解决方案。可以下载图表。

匿名用户

您可以使用De Bruijn序列获得一个肯定能解决魔方的序列(因为它将包含大小为20的所有可能排列)。

来自wiki(Python):

def de_bruijn(k, n):
    """
    De Bruijn sequence for alphabet k
    and subsequences of length n.
    """
    try:
        # let's see if k can be cast to an integer;
        # if so, make our alphabet a list
        _ = int(k)
        alphabet = list(map(str, range(k)))

    except (ValueError, TypeError):
        alphabet = k
        k = len(k)

    a = [0] * k * n
    sequence = []

    def db(t, p):
        if t > n:
            if n % p == 0:
                sequence.extend(a[1:p + 1])
        else:
            a[t] = a[t - p]
            db(t + 1, p)
            for j in range(a[t - p] + 1, k):
                a[t] = j
                db(t + 1, t)
    db(1, 1)
    return "".join(alphabet[i] for i in sequence)

你可以像这样使用它:

print(de_bruijn(x, 20))

其中20是序列的大小,x是包含立方体的每一个可能转弯(想不出更好的词)的列表/字符串。