Python中最短的数独求解器-它如何工作?


81

我在玩我自己的Sudoku求解器时,遇到以下问题时正在寻找一些指向快速好设计的指针:

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


21
看起来像是混淆的Perl竞赛的参赛作品!我认为python的要点之一就是编写清晰易懂的代码:)
沃伦

1
该python看起来好像缩进不正确。:/
杰克

18
这是您可以用任何语言编写难以理解的代码的又一证明。
JesperE

我认为这肯定是高尔夫球的答案。
洛伦·佩希特尔

2
顺便说一句,我很确定这是为编写最短的数独求解器而进行的竞赛。
约翰

Answers:


220

好了,您可以通过修复语法来使事情变得容易一些:

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,第一个空格由该字符串中的一个数字填充。但这仅在较早的表达式为false时才会发生。让我们看一下:

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'

1
...这只是表明,如果您真的努力的话,仍然可以用python编写错误的代码:-)
John Fouhy

2
只是为了清楚起见,您可能需要更改i%9/3 == j%9/3(i%9) / 3 == (j%9) / 3。我知道您应该完全了解运算符的顺序,但是它很容易忘记,而且扫描起来也容易一些。
Jordan Reiter

1
如果传递给函数的数字错误怎么办?这将永远消失还是在所有组合尝试后终止?
GundarsMēness2012年

2
@GundarsMēness在递归的每个点上,将处理一个空位置。如果找不到该位置的有效数字,该函数将简单返回。这意味着,如果找不到第一个空位的有效数字(即输入是无效的数独),则整个程序将不输出而返回(sys.exit(a)永远不会到达)
MartinStettner 2012年

5
@JoshBibb我知道这是一篇老文章,但是该错误正在您身上发生,因为它是为Python2编写的,并且您正在Python3中运行它。替换所有/的运营商same_rowsame_col以及same_block//运营商,你会得到正确的答案。
亚当·史密斯

10

不混淆:

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行是 退出,所以我觉得你的逻辑是相反的
鲍比·杰克

假设我= -1。然后〜i = 0,并且0或foo导致对foo求值。另一方面,如果i!= -1,则〜i将为非零值,因此or的第一部分为true,这会由于短路而导致or的第二个参数不被求值。评价。
Tetha

7

r(a)是一个递归函数,它尝试0在每个步骤中在电路板上填充一个。

i=a.find('0');~i or exit(a)是成功终止。如果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)]如果放入m当前明显不正确,则评估为真0。让我们将其昵称为“ is_bad”。这是最棘手的问题。:)

is_bad or r(a[:i]+m+a[i+1:]是有条件的递归步骤。0 如果当前的候选解决方案看起来不错,它将递归地尝试评估董事会中的下一个。

for m in '%d'%5**18 枚举从1到9的所有数字(无效)。


5

许多简短的数独求解器只是递归地尝试剩下的所有可能的合法数字,直到他们成功填满了单元格为止。我没有拆开它,只是略读了一下,看起来就是这样做的。


3

该代码实际上不起作用。您可以自己测试。这是一个未解决的数独难题示例:

807000003602080000000200900040005001000798000200100070004003000000040108300000506

您可以使用该网站(http://www.sudokuwiki.org/sudoku.htm),单击导入拼图,然后只需复制上面的字符串即可。python程序的输出为:817311213622482322322131224934443535441555798655266156777774663869988847188399979596

这与解决方案不符。实际上,您已经看到矛盾,第一行中有两个1。


1
好点子。您如何找到这样的难题?该求解器引发的难题中是否存在某种特征?
Ville Salonen 2014年

3
注意:它是用Python 2.7编写的,并且会产生正确的响应,即:897451623632987415415236987749325861163798254258164379584613792792542542321321879546.不要使用Python 3,因为分歧是不同的。
Beta Projects
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.