【问题标题】:Solving a 3x3 frog puzzle with python用 python 解决一个 3x3 青蛙谜题
【发布时间】:2020-12-24 12:41:36
【问题描述】:

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

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

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

【问题讨论】:

  • 您的问题是什么?并展示你的代码努力。
  • 有 9!x4^9 / 4 = 23,781,703,680 种方式来放下瓷砖(忽略整体布局的旋转)。蛮力方法可能需要比您预期的更长的时间。回溯方法会好得多。就瓦片的旋转而言,您可以定义一类具有方向属性的瓦片。或者——编写一个旋转 2x2 矩阵的函数很容易。
  • 拜托,看看my solution,虽然有点长,但它的目的是非常快,并且用于非常多数量的瓷砖和任意(非-square) 要覆盖的矩形形状。
  • 顺便说一句,我的脚本总共显示了您的任务的 8 种可能解决方案,用于图片上显示的瓷砖。如果不考虑整个董事会的所有 4 个轮换,则意味着您的任务总共有 2 个不同的解决方案。请参阅my answer 的结尾以查看这些解决方案的控制台 ASCII-art 输出。
  • @xvy。也许你有更多的瓷砖来完成这个青蛙任务?例如。 25 = 5x5。

标签: python numpy matrix backtracking tile


【解决方案1】:

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

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

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

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

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

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

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

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

另外忘了提一下,我的算法也可以运行以查找不是所有可能的解决方案,而是查找任何第一个解决方案,为了只找到第一个,只需将 True 作为 find 函数的最后一个参数传递,即运行 find(l, h, w) if如果您只想找到第一个解决方案,请运行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   #
###########################################################################################

【讨论】:

    【解决方案2】:

    正如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) 顶部瓷砖。如果左侧和/或上方没有图块,则忽略约束。两只相邻的青蛙必须颜色一致,方向相反。

    这是一个可能的实现:

    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 运行它,它很可能会失败。您应该改进此代码,以便在找不到下一个图块(即它到达内部循环的末尾)时回溯到以前的解决方案。此外,这不会考虑磁贴旋转,但您可以轻松地在内循环中添加此功能!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-10
      • 2013-09-24
      • 2019-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多