6

问题:我们有一个 5 行 4 列的方形网格。我们需要使用这些数字来填充网格;1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40. 我们需要以这样一种方式填充网格,即每个水平和垂直的邻居都应该无余地分割其他。例如,12and3可以是邻居,因为12 % 3 == 0, 但是5and 12 不能。网格 2x2 是10.

我尝试使用集合列表来解决问题。每个集合代表每个网格的可能值。当每个集合只有一个元素时,问题就解决了。这是我用来尝试解决这个问题的功能(我添加了整个东西以防万一,但我认为我的问题在于解决功能。);

class CannotSolveError(Exception):
    pass

def suitable_neighbor(a,b):
    "return True if a and b can be neighbors."
    return (a > b) and (a % b == 0) or (b % a == 0)

def equalize_tables(table1, table2):
    "Make two tables equal, by changing first one in-place"
    for i in range(len(table1)):
        table1[i] = table2[i]


def remove_possibility(table, row, column, value):
    """Remove possibilities that can't be neighbors with value in rowxcolumn grid."""

    index = ((row - 1) * num_cols) + column - 1

    if len(table[index]) == 1:
        return # This is a solved grid, do nothing.

    remaining_possibilities = set(
        filter(lambda x: suitable_neighbor(x, value), table[index])
                                )

    if not remaining_possibilities:
        raise ValueError("Invalid move")

    if len(remaining_possibilities) == 1:
        "Only one possibility remains, try to put it!"
        copy_table = table[:]
        try:
            "Try it on copy"
            put(copy_table, row, column, remaining_possibilities.pop())
        except ValueError:
            "Cannot put, re-raise and do nothing.."
            raise
        else:
            "Putting is successfull, update original table"
            equalize_tables(table, copy_table)
    else:
        table[index] = remaining_possibilities


def put(table, row, column, value):
    """Put a value on a grid, modifies given table. use with care!"""

    index = ((row - 1) * num_cols) + column - 1

    "Is this move possible?"
    if value not in table[index]:
        raise ValueError("Cannot put %d on %dx%d" % (value, row, column))

    "Remove possibilities from left neighbor"
    if column > 1:
        remove_possibility(table, row, column - 1, value)

    "Remove possibilities from right neighbor"
    if column < num_cols:
        remove_possibility(table, row, column + 1, value)

    "Remove possibilities from upper neighbor"
    if row > 1:
        remove_possibility(table, row - 1, column, value)

    "Remove possibilities from lower neighbor"
    if row < num_rows:
        remove_possibility(table, row + 1, column, value)

    "Remove this value from every other set."
    for i in range(num_rows * num_cols):
        if i == index:
            continue

        table[i].discard(value)

    "Put one-item set in place. Have to do this last."
    table[index] = set([value])



def solve(table):
    "Try to solve the table by trying possible values on grids."

    to_try = [(i,len(table[i])) for i in range(num_rows * num_cols) if len(table[i]) > 1]

    "Grid with least remaining possibilities will be tried first."
    to_try.sort(key = lambda x: x[1])

    for index, _ in to_try:
        for value in table[index]:
            row = index / num_cols + 1
            column = index % num_cols + 1
            copy_table = table[:]
            put(copy_table, row, column, value)
            try:
                solve(copy_table)
                equalize_tables(table, copy_table)
                return
            except CannotSolveError:
                continue
            except ValueError:
                continue
    raise CannotSolveError

我认为这个算法应该可以解决问题。但我超过了最大递归深度。任何想法如何解决这个问题,或者我应该如何在 Python 中更好地解决这个问题?

这不是一个家庭作业问题。我正在自己解决这个问题。

4

3 回答 3

6

为了避免炸毁你的堆栈,一个更健壮的方法是为你的部分解决方案(部分填充板)设计一个编码,并自己实现回溯。与依赖 python 的堆栈相比,这将需要更少的内存。

Google 的 Peter Norvig 撰写了一篇富有启发性的文章,描述了他如何使用这些技术来构建高效的回溯数独求解器。它使用一种他称之为“约束传播”的技术来限制选项的空间,以便可以通过蛮力回溯搜索快速找到解决方案(即,不检查每个可能的数字网格,而只追求可能仍然存在的部分网格导致解决方案)。我认为您会发现它非常适用,不仅适用于一般想法,而且适用于具体问题:您的问题,正如您所接近的那样,非常接近数独求解器。

于 2012-08-29T12:42:08.877 回答
2

有一种方法可以为 Python 递归限制设置自定义值(默认为 1000):

import sys
sys.setrecursionlimit(10000000)

您可以在递归调用之前添加这些行,如果问题仍然存在,您必须检查您的实现以查找其他可能的错误。

于 2012-08-29T12:12:46.997 回答
0

这是一个下雨天,所以我写了一个解决方案。如果您愿意,我可以发布,但也许您更愿意自己找到?

这里有一些提示:

  • 您的代码似乎不是在 (2,2) 以 10 开头

  • 尝试新值时,您可以将其添加到任何空白处。最好的尝试空间是有很多邻居的空间,因为这样可以让你快速测试和拒绝错误的值。

  • 假设上面,或者说同样的事情的不同方式 - 我的搜索超出了价值。所以我为“下一步”选择了一个位置,并在那里尝试了每个值。相反的是搜索位置(选择“下一个值”并在每个位置使用该值进行搜索),但效率不高(见上文)。

  • 回溯和重试时,始终遵循相同的位置模式。例如,(2,2) 是 10,那么 (2,3) 可能是 40,那么您可能找不到适合 (2,4) 的东西。所以你回溯并删除 40 并在 (2,3) 处尝试不同的数字。但是您尝试的第二个数字(在 10 之后,在 (2,2) 处)总是在 (2,3)。如果您不小心这样做,您最终可能会测试许多重复的组合。抱歉,不确定这很清楚。基本上 - 选择一个您在搜索和回溯时填写并坚持下去的“路径”。因为选择这条路径是为了最大化邻居的数量(上面的点),所以我在继续时构建了它,但保留了我在回溯时使用的路径位置的缓存。通过显示代码更容易解​​释......

  • 对于表,我使用了一个数组数组。复制时,我重新使用了未更改的列。这应该会减少内存使用(我不知道这是否重要)。

  • 搜索只需要递归 40 次(每个值一次),因此堆栈足够大。

  • 一个简单的搜索,在 python 中,依次尝试每个值,失败时回溯,在我的笔记本电脑上运行约 4 分钟(假设您使用上面的提示)(不打印稍微修改的版本只需 8 秒)。

  • 我发现有一个 python 函数很有用,给定一个网格和一个位置,返回yield邻居坐标的列表(嗯,一个生成器,带有)。这使得编写其他函数(例如测试移动是否正常的函数)更简单。

无论如何,如果您想要代码或解决方案(我将代码更改为全部打印但只有一个),请询问,我会发布。当然,它也可能有一个错误:o)

解决方案

我对此进行了一些调整,它现在打印出 (2,2)=10 解决方案,然后搜索所有解决方案(它仍在为我运行):

#!/usr/bin/python3

nx, ny = 4, 5
values = [1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40]
# grid[x][y] so it is a list of columns (prints misleadingly!)
grid = [[0 for _ in range(ny)] for _ in range(nx)]
# cache these to avoid re-calculating
xy_moves = {}
debug = False

def neighbours(grid, x, y):
    'coordinates of vertical/horizontal neighbours'
    for (xx, yy) in [(x-1,y),(x+1,y),(x,y-1),(x,y+1)]:
        if xx > -1 and xx < nx and yy > -1 and yy < ny:
            yield xx, yy

def filled_neighbours(grid, x, y):
    'filter "neighbours" to give only filled cells'
    return filter(lambda xy: grid[xy[0]][xy[1]], neighbours(grid, x, y))

def count_neighbours(grid, x, y):
    'use this to find most-constrained location'
    return sum(1 for _ in filled_neighbours(grid, x, y))

def next_xy(grid, depth):
    '''given a certain depth in the search, where should we move next?  
       choose a place with lots of neighbours so that we have good 
       constraints (and so can reject bad moves)'''
    if depth not in xy_moves:
        best, x, y = 0, nx // 2, ny // 2 # default to centre
        for xx in range(nx):
            for yy in range(ny):
                if not grid[xx][yy]:
                    count = count_neighbours(grid, xx, yy)
                    if count > best:
                        best, x, y = count, xx, yy
        xy_moves[depth] = (x, y)
        if debug: print('next move for %d is %d,%d' % (depth, x, y))
    return xy_moves[depth]

def drop_value(value, values):
    'remove value from the values'
    return [v for v in values if v != value]

def copy_grid(grid, x, y, value):
    'copy grid, replacing the value at x,y'
    return [[value if j == y else grid[i][j] for j in range(ny)]
            if x == i else grid[i]
            for i in range(nx)]

def move_ok(grid, x, y, value):
    'are all neighbours multiples?'
    for (xx, yy) in filled_neighbours(grid, x, y):
        g = grid[xx][yy]
        if (g > value and g % value) or (g < value and value % g):
            if debug: 
                print('fail: %d at %d,%d in %s' % (value, x, y, grid))
            return False
    return True

def search(grid, values, depth=0):
    'search over all values, backtracking on failure'
    if values:
        (x, y) = next_xy(grid, depth)
        for value in values:
            if move_ok(grid, x, y, value):
                if debug: print('add %d to %d,%d' % (value, x, y))
                for result in search(copy_grid(grid, x, y, value),
                                     drop_value(value, values), 
                                     depth+1):
                    yield result
    else:
        yield grid


# run the search, knowing that (2,2) (which is (1,1) for zero-indexing)
# has the value 10.
for result in search(copy_grid(grid, 1, 1, 10), drop_value(10, values)):
    print(result)

# how many solutions in total?
#xy_moves = {} # reset cache
#for (n, solution) in enumerate(search(grid, values)):
#    print('%d: %s' % (n, solution))

它首先选择将使用添加下一个数字的正方形next_xy()。它选择尽可能多的现有数字附近的位置,以便它可以有效地测试和拒绝数字(该位置被保存,xy_moves因此如果我们回溯就不需要重新找到它)。对于每个值,它会检查将值放在该位置是否可以使用move_ok. 如果是这样,它会计算一个新的网格(添加了值)和一个新的值列表(删除了使用的值)并递归。当没有值要添加时,递归结束。

这是结果(每个内部列表都是一个):

> time ./grid.py 
[[4, 20, 5, 35, 7], [40, 10, 30, 1, 21], [8, 2, 6, 18, 3], [24, 12, 36, 9, 27]]
real    0m5.909s

[删除了关于递归和生成器的错误评论]

更新

它完成了全局搜索——如果你一开始没有修复 (2,2),似乎总共有 12 个解决方案(如果你忽略简单的对称性,则有 3 个不同的解决方案)。

更新 2

最终代码,包括搜索没有对称重复的所有解决方案

于 2012-08-29T16:02:21.577 回答