Pythonで最短の数独ソルバー-どのように機能しますか?


81

私は自分の数独ソルバーで遊んでいて、これに出くわしたとき、優れた高速設計へのいくつかのポインターを探していました。

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つは、簡単に理解できるクリーンなコードを書くことだと思いました:)
warren

1
そのPythonは、正しくインデントされているようには見えません。:/
ジェイク

18
これは、理解できないコードをどの言語でも記述できることを示すもう1つの証拠です。
JesperE 2008年

これはコードゴルフの答えだったに違いないと思います。
Loren Pechtel 2009

2
ところで、これは可能な限り最短の数独ソルバーを書くための競争のためだったと確信しています。
ジョン

回答:


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回は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は、文字として表示される場合、可能な置換として除外されます。これは、最初の式がゼロの場合にのみ発生する可能性があります。式がゼロになるのはいつですか?

乗算される3つの部分があります。

  • (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の場合、つまり3行の同じブロックの場合はゼロです。
    • i%9/3^j%9/3 i%9/3 == j%9/3、つまり3列の同じブロックの場合はゼロです

これらの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は他の何かを選択できるポイントに戻って戻るので、これは基本的な深さ優先アルゴリズムであることに注意してください。

ヒューリスティックを使用しないため、特に効率的ではありません。私はウィキペディア(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。演算子の順序を覚えておく必要があることは知っていますが、忘れがちで、スキャンが少し簡単になります。
ジョーダンライター2011年

1
関数に渡された数値が間違っている場合はどうなりますか?これは永遠に続くのでしょうか、それともすべての組み合わせが試行された後に自動的に終了するのでしょうか?
GundarsMēness

2
@GundarsMēness再帰の各ポイントで、単一の空の位置が処理されます。この位置に有効な数字が見つからない場合、関数は単に戻ります。つまり、最初の空の位置の有効な数字が見つからない場合(つまり、入力が無効な数独だった場合)、プログラム全体が出力なしで返されます(sys.exit(a)到達することはありません)
MartinStettner 2012年

5
@JoshBibbこれは古い投稿ですが、これはPython2用に作成されており、Python3で実行しているため、このエラーが発生しています。すべての置き換え/の演算子same_rowsame_colsame_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行目は終了するか、終了するので、ロジックが逆になっていると思います
Bobby Jack

i = -1と仮定します。次に、〜i = 0であり、0またはfooを指定すると、fooが評価されます。一方、i!= -1の場合、〜iはゼロ以外になるため、またはの最初の部分が真になり、短絡により、またはの2番目のパラメーターが評価されなくなります。評価。
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プログラムの出力は次のとおりです。817311213622482322131224934443535441555798655266156777774663869988847188399979596

これは解決策に対応していません。実際、最初の行に2つの1という矛盾がすでに見られます。


1
いい視点ね。どうやってそのようなパズルを見つけたのですか?このソルバーを投げるパズルには、ある種の特徴がありますか?
Ville Salonen 2014

3
注意:Python 2.7で記述されており、正しい応答が生成されます:897451623632987415415236987749325861163798254258164379584613792976542138321879546。除算が異なるため、Python3を使用しないでください。
ベータプロジェクト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.