我有一个Knuth算法的实现(“跳舞链接”),该实现的行为非常奇怪。我找到了一种解决方法,但这就像魔术。下面的脚本测试有关N Queens问题的代码。该错误出现在第一个函数solve
中。参数limit
用于限制生成的解决方案的数量,默认值为0表示“生成所有解决方案”。
#Test generalized exact cover for n queens problem
def solve(Cols, Rows, SecondaryIDs=set(), limit = 0):
for soln in solver(Cols, Rows, SecondaryIDs):
print('solve:', limit, soln)
yield soln
limit -= 1
if limit == 0: return
def solver(Cols, Rows, SecondaryIDs, solution=[]):
live=[col for col in Cols if col not in SecondaryIDs]
if not live:
yield solution
else:
col = min(live, key = lambda col: len(Cols[col]))
for row in list(Cols[col]):
solution.append(row)
columns = select(Cols, Rows, row)
for soln in solver(Cols, Rows, SecondaryIDs, solution):
yield soln
deselect(Cols, Rows, row, columns)
solution.pop()
def select(Cols, Rows, row):
columns = []
for col in Rows[row]:
for rrow in Cols[col]:
for ccol in Rows[rrow]:
if ccol != col:
Cols[ccol].remove(rrow)
columns.append(Cols.pop(col))
return columns
def deselect(Cols, Rows, row, columns):
for col in reversed(Rows[row]):
Cols[col] = columns.pop()
for rrow in Cols[col]:
for ccol in Rows[rrow]:
if ccol != col:
Cols[ccol].add(rrow)
n = 5
# From Dancing Links paper
solutionCounts = {4:2, 5:10, 6:4, 7:40, 8:92, 9:352, 10:724}
def makeRows(n):
# There is one row for each cell.
rows = dict()
for rank in range(n):
for file in range(n):
rows["R%dF%d"%(rank,file)] = ["R%d"%rank, "F%d"%file, "S%d"%(rank+file), "D%d"%(rank-file)]
return rows
def makePrimary(n):
# One primary column for each rank and file
prim = dict()
for rank in range(n):
prim["R%d"%rank] = {"R%dF%d"%(rank,file) for file in range(n)}
for file in range(n):
prim["F%d"%file] = {"R%dF%d"%(rank,file) for rank in range(n)}
return prim
def makeSecondary(n):
# One secondary column for each diagonal
second = dict()
for s in range(2*n-1):
second["S%d"%s] = {"R%dF%d"%(r, s-r) for r in range(max(0,s-n+1), min(s+1,n))}
for d in range(-n+1, n):
second["D%d"%(-d)]={"R%dF%d"%(r, r+d) for r in range(max(0,-d),min(n-d, n))}
return second
rows = makeRows(n)
primary = makePrimary(n)
secondary = makeSecondary(n)
primary.update(secondary)
secondary = secondary.keys()
#for soln in solve(primary, rows, secondary, 15):
#print(soln)
solutions = [s for s in solve(primary, rows, secondary)]
try:
assert len(solutions) == solutionCounts[n]
except AssertionError:
print("actual %d expected %d"%(len(solutions), solutionCounts[n]))
for soln in solutions:print(soln)
该代码设置为生成5个皇后问题的前6个解决方案,并且工作正常。 (查看通话
solutions = [s for s in solve(primary, rows, secondary, 6)]
在第80行。)实际上有10个解决方案,如果我要10个解决方案,我将全部解决。如果我超出了限制,那么呼叫是
solutions = [s for s in solve(primary, rows, secondary)]
主程序打印十个空列表[]
作为解决方案,但是solve
中的代码打印实际的解决方案。如果我将极限设为15,也会发生同样的事情。
当我将生成器转换为第80行的列表时,似乎会出现问题。如果我将第78和79行的注释掉的行放回去,然后注释掉第80行的所有内容,程序将按预期运行。但是我不明白这一点。我经常以这种方式列出生成器返回的对象。
另一件更奇怪的事情是,如果我将第13行更改为
yield list(solution)
然后从80行开始的代码在所有情况下都可以正常工作。我不记得当我最初编写代码时是如何偶然发现这种混乱的。我今天在看它,然后将yield list(solution)
更改为yield solution
,这时该bug才显现出来。我听不懂solution
已经是一个列表。实际上,我已经尝试添加行
assert solution == list(solution)
就在第13行之前,它从不引发AssertionError
。
我完全不知所措。我试图制作一个较小的脚本来重现此行为,但我一直无法。您了解发生了什么,(更难)可以向我解释吗?
答案 0 :(得分:4)
在看到代码之前进行预测:yield list(solution)
会产生浅层的副本解决方案。 yield solution
生成解决方案列表本身,因此,当您随后更改该列表时,就会遇到麻烦。
看来我是对的。 :-)较短的版本:
def weird(solution):
for i in range(len(solution)):
yield solution
solution.pop()
给出:
In [8]: result = list(weird(['a','b','c']))
In [9]: result
Out[9]: [[], [], []]
因为
In [10]: [id(x) for x in result]
Out[10]: [140436644005128, 140436644005128, 140436644005128]
但是如果我们改为yield list(solution)
,我们会得到
In [15]: list(less_weird(['a','b','c']))
Out[15]: [['a', 'b', 'c'], ['a', 'b'], ['a']]
首先,我们看到一个可变的默认参数,这是一个坏主意,但实际上并不是导致您看到错误的原因:
def solver(Cols, Rows, SecondaryIDs, solution=[]):
live=[col for col in Cols if col not in SecondaryIDs]
if not live:
yield solution
在这里给出解决方案^ ..
else:
col = min(live, key = lambda col: len(Cols[col]))
for row in list(Cols[col]):
solution.append(row)
columns = select(Cols, Rows, row)
for soln in solver(Cols, Rows, SecondaryIDs, solution):
yield soln
deselect(Cols, Rows, row, columns)
solution.pop()
在这里,您将与您之前产生的相同列表进行变异。
答案 1 :(得分:3)
yield solution
问题在于您要生成一个列表,随后可以从中添加和删除项目。到呼叫者检查列表时,它已更改。您需要返回解决方案的冻结副本,以确保保留每个yield
语句处的结果。这些中的任何一个都可以:
yield list(solution)
yield solution[:]
yield tuple(solution)