如何摆脱最大递归深度误差或更好地解决这个问题?

时间:2012-08-29 12:10:34

标签: python algorithm recursion

问题:我们有一个5行4列的正方形网格。我们需要使用这些数字来填充网格; 1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40。我们需要以这样的方式填充网格,即每个水平和垂直邻居应该在没有余数的情况下划分其他邻居。例如,123可以是邻居,因为12 % 3 == 0,但5和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中更好地解决这个问题?

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

3 个答案:

答案 0 :(得分:6)

为避免炸毁堆栈,更强大的方法是为部分解决方案设计编码(部分填充板),并自行实现回溯。这将需要比依赖python的堆栈少得多的内存。

Google的Peter Norvig写了一篇很有启发性的文章,描述了他如何利用这些技术建立有效的回溯sudoku solver。它使用他称之为“约束传播”的技术来限制选项的空间,这样就可以通过强力回溯搜索快速找到解决方案(也就是说,无需检查每个可能的数字网格,但是只追求可能仍然导致解决方案的部分网格)。我认为你会发现它非常适用,不仅适用于一般的想法,也适用于具体的问题:你的问题,正如你已接近它,非常接近数独求解器。

答案 1 :(得分:2)

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

import sys
sys.setrecursionlimit(10000000)

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

答案 2 :(得分: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

final code, including search for all solutions without symmetrical duplicates