用 python 解决 3x3 青蛙拼图

Solving a 3x3 frog puzzle with python

这是一个谜题,您的目标是将青蛙头部与内边缘的身体配对。随附的图片显示了已解决的难题。

我已经在Python中思考了如何解决这个难题。我的想法是将图块表示为 2x2 numpy 矩阵的数组,例如 [["RB", "GB"], ["BB", "GH"]],然后循环遍历所有排列并检查青蛙是否在边缘匹配。 然而,这种方法不会考虑旋转,这可以在单个矩阵上使用 numpy.rot90() 来完成。

我不知道这是否是一个可行的解决方案,或者我是否采取了错误的方法来解决这个问题。

你的任务很有趣。我实现了我自己的解决方案,它应该非常快,因为它使用 Numba 通常将 Python 代码平均提升 50x-200x 倍。

我的算法基于 backtracking 方法,并且是非递归的(它使用堆栈)。

它查找并打印所有可能的解决方案,而不仅仅是第一个。请参阅我的答案结尾以查看控制台中的结果输出。首先打印出一些解决方案,然后以 ASCII 图形形式打印所有解决方案。

而且我的算法是通用的,这意味着你可以提供尽可能多的瓷砖,提供的瓷砖数量可以大于要覆盖的矩形(但不能少于),矩形不限于 3x3,它可以具有任意高度和宽度,宽度和高度也可能不相等。例如。您可以使用 20 个瓷砖来覆盖形状为 4x3 的矩形,这样 12 个瓷砖将用于解决方案,8 个未使用。

算法的输入数据位于test()函数内。它调用 find(l, h, w) 函数,其中 a 是所有现有图块的列表,hw 是要覆盖的矩形的高度和宽度(以图块计)。

输入图块采用以下格式:每个图块应恰好有 4 个字符串元素,每个字符串元素应恰好 2 个字符,第一个字符表示颜色,r:红色,g:绿色,b:蓝色,h:棕色;第二个字符是青蛙的一面,b:底部,t:顶部(例如 'bt' 表示青蛙的蓝色顶部)。 4个元素表示瓷砖的4个面,第一:右,第二:上,第三:左,第四:下。

为了 运行 我的脚本,你必须通过命令行一次安装两个模块 python -m pip install numpy numba

我希望感谢回溯和 Numba,我的算法可以解决很多图块的任务。

还忘了说,我的算法也可以是 运行 不是找到所有可能的解决方案,而是找到任何第一个解决方案,为了只找到第一个,只需将 True 作为 find 函数的最后一个参数,即 运行 find(l, h, w) 如果你想找到所有的解决方案和 运行 find(l, h, w, True) 如果你只想要第一个。

Try following code online!

import numpy as np, numba

@numba.njit(cache = True)
def nfind(a, h, w, first):
    hw = h * w
    n = a.shape[0]
    taken = np.zeros((n,), dtype = np.bool_)
    rot = np.zeros((h, w), dtype = np.int32)
    idx = np.zeros((h, w), dtype = np.int32)
    stack = np.zeros((hw, 2), dtype = np.int32)
    ans = np.zeros((0, h, w, 2), dtype = np.int32)
    i, r, istack = 0, 0, 0
    while True:
        y, x = istack // w, istack % w
        if i >= n or istack >= hw:
            if istack >= hw:
                cans = np.zeros((1, h, w, 2), dtype = np.int32)
                for i0 in range(h):
                    for i1 in range(w):
                        cans[0, i0, i1] = idx[i0, i1], rot[i0, i1]
                ans = np.concatenate((ans, cans))
                if first:
                    return ans
            istack -= 1
            if istack < 0:
                return ans
            i, r = stack[istack]
            taken[i] = False
            i, r = i + (r + 1) // 4, (r + 1) % 4
        elif (
            not taken[i] and
            (y == 0 or a[idx[y - 1, x], (rot[y - 1, x] + 3) % 4] == a[i, (r + 1) % 4] ^ 1) and
            (x == 0 or a[idx[y, x - 1], (rot[y, x - 1] + 0) % 4] == a[i, (r + 2) % 4] ^ 1)
        ):
            stack[istack] = i, r
            taken[i] = True
            idx[y, x] = i
            rot[y, x] = r
            istack += 1
            i, r = 0, 0
        else:
            i, r = i + (r + 1) // 4, (r + 1) % 4

def find(l, h, w, first = False):
    a = np.zeros((len(l), 4), dtype = np.uint8)
    col, sid = 'rgbh', 'bt'
    for i, x in enumerate(l):
        assert len(x) == 4, x
        for j, y in enumerate(x):
            assert len(y) == 2, y
            a[i, j] = (col.index(y[0]) << 1) | sid.index(y[1])
    r = nfind(a, h, w, first)
    print('Number of solutions: ', r.shape[0], '\n')
    s = []
    for i in range(r.shape[0]):
        ss = []
        for y in range(h):
            sss = []
            for x in range(w):
                e = []
                for j in range(4):
                    e += [l[r[i, y, x, 0]][(r[i, y, x, 1] + j) % 4]]
                sss += [[
                    f'  {e[1]}  ',
                    f'{e[2]}  {e[0]}',
                    f'  {e[3]}  ',
                ]]
            ss += [sss]
        s += [ss]
    bl = 4
    for i in range(0, len(s), bl):
        lines = [''] * (len(s[0]) * 4 - 1)
        for ie, e in enumerate(s[i : i + bl]):
            for y in range(len(s[0])):
                for x in range(len(s[0][0])):
                    for il, l in enumerate(e[y][x]):
                        lines[y * 4 + il] += l + ('|', ' # ')[x + 1 >= len(s[0][0])]
                    if y + 1 < len(s[0]):
                        lines[y * 4 + 3] += '-' * (7, 6)[x + 1 >= len(s[0][0])]
                        if x + 1 >= len(s[0][0]):
                            lines[y * 4 + 3] += ' # '
        lines += ['#' * (len(lines[0]) - 1)]
        for l in lines:
            print(l)
            
def test():
    find([
        ['gt', 'bt', 'bb', 'rb'], ['bt', 'hb', 'gb', 'rt'], ['bt', 'rb', 'bb', 'ht'],
        ['bb', 'rt', 'gt', 'hb'], ['bb', 'rb', 'bt', 'gt'], ['rb', 'hb', 'bt', 'gt'],
        ['rb', 'ht', 'gt', 'hb'], ['hb', 'gb', 'rt', 'gt'], ['rb', 'gb', 'ht', 'bt'],
    ], 3, 3)

if __name__ == '__main__':
    test()

控制台输出:

Number of solutions:  8

  bt  |  hb  |  rb   #   gt  |  gb  |  rb   #   bt  |  rb  |  rb   #   gt  |  bt  |  rb   #
bb  gt|gb  bt|bb  bt # bt  rb|rt  hb|ht  hb # rb  ht|hb  gt|gb  bt # hb  rt|rb  ht|hb  gt #
  rb  |  rt  |  ht   #   bb  |  gt  |  gt   #   bb  |  bt  |  ht   #   bb  |  bb  |  bt   #
-------------------- # -------------------- # -------------------- # -------------------- #
  rt  |  rb  |  hb   #   bt  |  gb  |  gb   #   bt  |  bb  |  hb   #   bt  |  bt  |  bb   #
gt  bb|bt  bb|bt  rb # gt  rb|rt  hb|ht  rb # hb  rt|rb  gt|gb  gt # rb  ht|hb  rt|rb  gt #
  hb  |  gt  |  gt   #   bb  |  bt  |  bt   #   gb  |  bt  |  rt   #   gb  |  gb  |  bt   #
-------------------- # -------------------- # -------------------- # -------------------- #
  ht  |  gb  |  gb   #   bt  |  bb  |  bb   #   gt  |  bb  |  rb   #   gt  |  gt  |  bb   #
gt  rb|rt  hb|ht  rb # gt  hb|ht  rb|rt  hb # bt  rb|rt  hb|ht  hb # hb  ht|hb  rt|rb  bt #
  hb  |  gt  |  bt   #   rb  |  bt  |  gt   #   bb  |  gt  |  gt   #   rb  |  gb  |  gt   #
###########################################################################################
  gt  |  gt  |  bt   #   gt  |  gt  |  bb   #   hb  |  rb  |  hb   #   bt  |  gt  |  hb   #
rb  bt|bb  bt|bb  gt # hb  ht|hb  rt|rb  bt # rb  gt|gb  bt|bb  gt # rb  ht|hb  rt|rb  gt #
  hb  |  rb  |  rb   #   rb  |  bb  |  gt   #   ht  |  ht  |  rt   #   gb  |  gb  |  ht   #
-------------------- # -------------------- # -------------------- # -------------------- #
  ht  |  rt  |  rt   #   rt  |  bt  |  gb   #   hb  |  hb  |  rb   #   gt  |  gt  |  hb   #
bt  bb|bt  gb|gt  gb # gt  gb|gt  rb|rt  hb # gb  gt|gb  bt|bb  bt # rb  bt|bb  bt|bb  gt #
  rb  |  hb  |  hb   #   hb  |  bb  |  bt   #   rt  |  rt  |  ht   #   hb  |  rb  |  rt   #
-------------------- # -------------------- # -------------------- # -------------------- #
  rt  |  ht  |  ht   #   ht  |  bt  |  bb   #   rb  |  rb  |  hb   #   ht  |  rt  |  rb   #
gt  bb|bt  gb|gt  rb # bt  gb|gt  hb|ht  rb # gt  bb|bt  bb|bt  rb # bt  bb|bt  gb|gt  bb #
  hb  |  rb  |  hb   #   rb  |  rb  |  bt   #   bt  |  gt  |  gt   #   rb  |  hb  |  bt   #
###########################################################################################

正如John Coleman所说,暴力破解不是一个好的策略。我尝试了一下。这有望让您开始使用迭代方法,逐步构建解决方案。

将图块表示为二元组会更容易:

  • 瓷砖颜色:'G' 绿色,'P' 紫色 - 更像蓝色,'R' 红色,'B' 棕色。
  • 平铺方向:0 青蛙向外看,1 青蛙向内看(朝向平铺的中心)。

例如,您的编码 'RB' 转换为 ('R', 0)

所以这是前两行:

tiles = [
    [('P', 0), ('P', 1), ('G', 1), ('R', 0)],
    [('G', 0), ('B', 0), ('P', 1), ('R', 1)],
    [('P', 0), ('R', 0), ('P', 1), ('B', 1)],
    [('G', 1), ('R', 1), ('P', 0), ('B', 0)],
    [('P', 1), ('R', 0), ('P', 0), ('G', 1)],
    [('P', 1), ('B', 0), ('R', 0), ('G', 1)],
]

棋盘将表示为二维数组:

np.zeros((2, 3), dtype='object')`

我们将遍历图板并针对每个位置(从左到右,从上到下),通过遍历可用的图块来搜索合适的图块。放置新瓷砖时,有两个水平约束 (i):瓷砖在左侧,(ii) 瓷砖在上方。如果上方左侧 and/or 没有磁贴,则忽略约束。两只相邻的青蛙必须颜色匹配且方向相反。

这是一个可能的实现:

for (i, j), _ in np.ndenumerate(board):
    above = None if i == 0 else board[i-1, j][3] # vertical constraint
    left = None if j == 0 else board[i, j-1][2] # horizontal constraint

    for t, tile in enumerate(tiles):
        (c_left, dir_left), (c_up, dir_up), *_ = tile

        if ((not left or (left[0] == c_left and left[1] != dir_left)) and 
            (not above or (above[0] == c_up and above[1] != dir_up))):
            board[i, j] = tile
            tiles.pop(t)

注意:如果你 运行 使用随机 tiles list 它很可能会失败。您应该改进此代码,以便在找不到下一个图块(即它到达内部循环的末尾)时回溯到先前的解决方案。此外,这不会考虑图块旋转,但您可以在内部循环中轻松添加此功能!