问题:我们有一个5行4列的正方形网格。我们需要使用这些数字来填充网格; 1,2,3,4,5,6,7,8,9,10,12,18,20,21,24,27,30,35,36,40
。我们需要以这样的方式填充网格,即每个水平和垂直邻居应该在没有余数的情况下划分其他邻居。例如,12
和3
可以是邻居,因为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中更好地解决这个问题?
这不是一个家庭作业问题。我自己正在研究这个问题。
答案 0 :(得分:6)
为避免炸毁堆栈,更强大的方法是为部分解决方案设计编码(部分填充板),并自行实现回溯。这将需要比依赖python的堆栈少得多的内存。
Google的Peter Norvig写了一篇很有启发性的文章,描述了他如何利用这些技术建立有效的回溯sudoku solver。它使用他称之为“约束传播”的技术来限制选项的空间,这样就可以通过强力回溯搜索快速找到解决方案(也就是说,无需检查每个可能的数字网格,但是只追求可能仍然导致解决方案的部分网格)。我认为你会发现它非常适用,不仅适用于一般的想法,也适用于具体的问题:你的问题,正如你已接近它,非常接近数独求解器。答案 1 :(得分:2)
有一种方法可以为Python递归限制设置自定义值(默认情况下为1000):
import sys
sys.setrecursionlimit(10000000)
您可以在递归调用之前添加这些行,如果问题仍然存在,则必须检查实现是否存在其他可能的错误。
答案 2 :(得分:0)
这是一个下雨天,所以我写了一个解决方案。如果你愿意,我可以发帖,但也许你宁愿自己找到它?
这里有一些提示:
您的代码似乎不是以(2,2)
尝试新值时,可以将其添加到任何空白区域。尝试的最佳空间是拥有大量邻居的空间,因为这样可以让您快速测试和拒绝不良值。
,或者说同一件事的不同方式 - 我的搜索超过了价值观。所以我为“下一步行动”选择了一个位置并在那里尝试了每个值。相反的是搜索位置(选择“下一个值”并在每个位置搜索该值),但效率不高(见上文)。
在回溯和重新尝试时,始终遵循相同的位置模式。例如,(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