Python 中最短的数独求解器 - 它是如何工作的?
Posted
技术标签:
【中文标题】Python 中最短的数独求解器 - 它是如何工作的?【英文标题】:Shortest Sudoku Solver in Python - How does it work? 【发布时间】:2010-09-17 02:50:59 【问题描述】:我正在玩自己的数独求解器,并正在寻找一些指向良好和快速设计的指针,当我遇到这个时:
def r(a):i=a.find('0');~i or exit(a);[m
in[(i-j)%9*(i/9^j/9)*(i/27^j/27|i%9/3^j%9/3)or a[j]for
j in range(81)]or r(a[:i]+m+a[i+1:])for m in'%d'%5**18]
from sys import*;r(argv[1])
我自己的实现解决数独问题的方式与我在脑海中解决数独问题的方式相同,但是这个神秘的算法是如何工作的?
http://scottkirkwood.blogspot.com/2006/07/shortest-sudoku-solver-in-python.html
【问题讨论】:
这看起来像是混淆 perl 竞赛的参赛作品!我认为python的要点之一是编写易于理解的干净代码:) 那个 python 看起来不像它的缩进正确。 :// 这再次证明您可以用任何语言编写难以理解的代码。 我认为这一定是一个代码高尔夫答案。 顺便说一句,我很确定这是为了编写尽可能短的数独求解器的竞赛。 【参考方案1】:好吧,你可以通过修正语法让事情变得更简单:
def r(a):
i = a.find('0')
~i or exit(a)
[m in[(i-j)%9*(i/9^j/9)*(i/27^j/27|i%9/3^j%9/3)or a[j]for j in range(81)] or r(a[:i]+m+a[i+1:])for m in'%d'%5**18]
from sys import *
r(argv[1])
清理一下:
from sys import exit, argv
def r(a):
i = a.find('0')
if i == -1:
exit(a)
for m in '%d' % 5**18:
m in[(i-j)%9*(i/9^j/9)*(i/27^j/27|i%9/3^j%9/3) or a[j] for j in range(81)] or r(a[:i]+m+a[i+1:])
r(argv[1])
好的,所以这个脚本需要一个命令行参数,并在其上调用函数 r。如果该字符串中没有零,则 r 退出并打印出它的参数。
(如果传递了另一种类型的对象, None 相当于通过零, 和任何其他对象打印到 sys.stderr 并导致退出 代码 1. 特别是, sys.exit("some error message") 是一个 快速退出程序的方法 发生错误。看 http://www.python.org/doc/2.5.2/lib/module-sys.html)
我猜这意味着零对应于开放空间,并且解决了没有零的难题。然后就是那个讨厌的递归表达式。
循环很有趣:for m in'%d'%5**18
为什么是 5**18?事实证明,'%d'%5**18
的计算结果为 '3814697265625'
。这是一个每个数字至少有一次 1-9 的字符串,所以它可能正在尝试放置每个数字。事实上,r(a[:i]+m+a[i+1:])
看起来就是这样:递归调用 r,第一个空白由该字符串中的一个数字填充。但这只发生在前面的表达式为假的情况下。让我们看看:
m in [(i-j)%9*(i/9^j/9)*(i/27^j/27|i%9/3^j%9/3) or a[j] for j in range(81)]
因此,只有当 m 不在该怪物列表中时,才会完成放置。每个元素要么是一个数字(如果第一个表达式为非零)或一个字符(如果第一个表达式为零)。如果 m 作为字符出现,则排除它作为可能的替换,这仅在第一个表达式为零时才会发生。什么时候表达式为零?
它有三个相乘的部分:
(i-j)%9
如果 i 和 j 是 9 的倍数,则为零,即同一列。
(i/9^j/9)
如果 i/9 == j/9 则为零,即同一行。
(i/27^j/27|i%9/3^j%9/3)
如果这两个都为零,则为零:
i/27^j^27
如果 i/27 == j/27 则为零,即相同的三行块
i%9/3^j%9/3
如果 i%9/3 == j%9/3 则为零,即相同的三列块
如果这三个部分中的任何一个为零,则整个表达式为零。换句话说,如果 i 和 j 共享行、列或 3x3 块,则 j 的值不能用作 i 处空白的候选。啊哈!
from sys import exit, argv
def r(a):
i = a.find('0')
if i == -1:
exit(a)
for m in '3814697265625':
okay = True
for j in range(81):
if (i-j)%9 == 0 or (i/9 == j/9) or (i/27 == j/27 and i%9/3 == j%9/3):
if a[j] == m:
okay = False
break
if okay:
# At this point, m is not excluded by any row, column, or block, so let's place it and recurse
r(a[:i]+m+a[i+1:])
r(argv[1])
请注意,如果所有位置都无法解决,r 将返回并返回到可以选择其他位置的位置,因此这是一种基本的深度优先算法。
不使用任何启发式方法,它不是特别有效。我从 Wikipedia (http://en.wikipedia.org/wiki/Sudoku) 中获取了这个谜题:
$ time python sudoku.py 530070000600195000098000060800060003400803001700020006060000280000419005000080079
534678912672195348198342567859761423426853791713924856961537284287419635345286179
real 0m47.881s
user 0m47.223s
sys 0m0.137s
附录:作为维护程序员,我将如何重写它(这个版本有大约 93 倍的加速:)
import sys
def same_row(i,j): return (i/9 == j/9)
def same_col(i,j): return (i-j) % 9 == 0
def same_block(i,j): return (i/27 == j/27 and i%9/3 == j%9/3)
def r(a):
i = a.find('0')
if i == -1:
sys.exit(a)
excluded_numbers = set()
for j in range(81):
if same_row(i,j) or same_col(i,j) or same_block(i,j):
excluded_numbers.add(a[j])
for m in '123456789':
if m not in excluded_numbers:
# At this point, m is not excluded by any row, column, or block, so let's place it and recurse
r(a[:i]+m+a[i+1:])
if __name__ == '__main__':
if len(sys.argv) == 2 and len(sys.argv[1]) == 81:
r(sys.argv[1])
else:
print 'Usage: python sudoku.py puzzle'
print ' where puzzle is an 81 character string representing the puzzle read left-to-right, top-to-bottom, and 0 is a blank'
【讨论】:
...这只是表明,如果你真的很努力,你仍然可以在 python 中编写糟糕的代码:-) 为了明确起见,您可能希望将i%9/3 == j%9/3
更改为(i%9) / 3 == (j%9) / 3
。我知道您应该牢记运算符的顺序,但它很容易忘记并且更容易扫描。
如果传递给函数的数字有误怎么办?这会永远持续下去还是会在尝试所有组合后自行终止?
@GundarsMēness 在递归的每个点,都会处理一个空位置。如果找不到该位置的有效数字,则函数简单地返回。这意味着,如果找不到第一个空位置的有效数字(即输入是无效的数独),则整个程序将返回而不输出(永远不会达到sys.exit(a)
)
@JoshBibbb 我知道这是一篇旧帖子,但您正在发生该错误,因为这是为 Python2 编写的,而您正在 Python3 中运行它。将same_row
、same_col
和same_block
中的所有/
运算符替换为//
运算符,您将得到正确答案。【参考方案2】:
不混淆它:
def r(a):
i = a.find('0') # returns -1 on fail, index otherwise
~i or exit(a) # ~(-1) == 0, anthing else is not 0
# thus: if i == -1: exit(a)
inner_lexp = [ (i-j)%9*(i/9 ^ j/9)*(i/27 ^ j/27 | i%9/3 ^ j%9/3) or a[j]
for j in range(81)] # r appears to be a string of 81
# characters with 0 for empty and 1-9
# otherwise
[m in inner_lexp or r(a[:i]+m+a[i+1:]) for m in'%d'%5**18] # recurse
# trying all possible digits for that empty field
# if m is not in the inner lexp
from sys import *
r(argv[1]) # thus, a is some string
所以,我们只需要计算出内部列表表达式。我知道它收集了行中设置的数字——否则,它周围的代码毫无意义。但是,我不知道它是如何做到的(而且我现在太累了,无法解决这种二进制幻想,抱歉)
【讨论】:
我不是python专家,但是第3行是或退出,所以我认为你的逻辑是相反的 假设 i = -1。然后 ~i = 0,并且 0 或 foo 导致 foo 被评估。另一方面,如果 i != -1,则 ~i 将不为零,因此, or 的第一部分将为真,这导致 or 的第二个参数由于短路而未被评估评估。【参考方案3】:r(a)
是一个递归函数,它试图在每一步中填写一个0
。
i=a.find('0');~i or exit(a)
是成功终止。如果板上没有更多的0
值,我们就完成了。
m
是我们将尝试填充0
的当前值。
如果将m
放入当前0
显然不正确,m
in[(i-j)%9*(i/9^j/9)*(i/27^j/27|i%9/3^j%9/3)or a[j]for
j in range(81)]
的计算结果为真。让我们给它起个绰号“is_bad”。这是最棘手的一点。 :)
is_bad or r(a[:i]+m+a[i+1:]
是一个条件递归步骤。如果当前候选解决方案看起来很正常,它将递归地尝试评估董事会中的下一个0
。
for m in '%d'%5**18
枚举从 1 到 9 的所有数字(效率低下)。
【讨论】:
他本可以使用 14 ** 7 * 9 而不是 5 ** 18。【参考方案4】:许多短数独求解器只是递归地尝试所有可能的合法数字,直到他们成功填充单元格。我没有把它拆开,只是略读一下,它看起来就是它的作用。
【讨论】:
【参考方案5】:代码实际上不起作用。你可以自己测试一下。这是一个未解决的数独谜题示例:
807000003602080000000200900040005001000798000200100070004003000000040108300000506
您可以使用这个网站 (http://www.sudokuwiki.org/sudoku.htm),点击导入拼图并简单地复制上面的字符串。 python程序的输出是: 817311213622482322131224934443535441555798655266156777774663869988847188399979596
这与解决方案不对应。事实上,你已经可以看到一个矛盾,第一行有两个 1。
【讨论】:
好点。你是怎么找到这样一个谜题的?这个求解器抛出的谜题有什么特点吗? 小心:它是用 Python 2.7 编写的,并产生正确的响应,即:897451623632987415415236987749325861163798254258164379584613792976542138321879546。不要将 Python 3 用作以上是关于Python 中最短的数独求解器 - 它是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章