파이썬에서 가장 짧은 스도쿠 솔버-어떻게 작동합니까?


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
난독 화 된 펄 콘테스트의 항목처럼 보입니다! 파이썬의 요점 중 하나가 쉽게 이해할 수있는 깨끗한 코드를 작성하는 것이라고 생각했습니다. :)
warren

1
그 파이썬은 제대로 들여 쓰기되지 않은 것 같습니다. : /
Jake

18
이것은 어떤 언어로든 이해할 수없는 코드를 작성할 수 있다는 또 다른 증거입니다.
JesperE

나는 이것이 코드 골프 답이었을 것이라고 생각합니다.
Loren Pechtel

2
BTW 나는 이것이 가능한 가장 짧은 스도쿠 해결사를 작성하는 경쟁을위한 것이라고 확신합니다.
John

답변:


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을 호출합니다. 해당 문자열에 0이 없으면 r이 종료되고 인수를 인쇄합니다.

(다른 유형의 객체가 전달되면 None은 0을 전달하는 것과 동일하며 다른 객체는 sys.stderr에 인쇄되고 종료 코드 1이됩니다. 특히 sys.exit ( "some error message")는 오류 발생시 프로그램을 종료하는 빠른 방법. http://www.python.org/doc/2.5.2/lib/module-sys.html 참조 )

이것은 0이 열린 공간에 해당하고 0이없는 퍼즐이 해결된다는 것을 의미한다고 생각합니다. 그런 다음 불쾌한 재귀 표현이 있습니다.

루프는 흥미 롭습니다. 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이 해당 몬스터 목록에없는 경우에만 수행됩니다. 각 요소는 숫자 (첫 번째 표현식이 0이 아닌 경우) 또는 문자 (첫 번째 표현식이 0 인 경우)입니다. m은 문자로 표시되는 경우 가능한 대체로 제외되며 첫 번째 표현식이 0 인 경우에만 발생할 수 있습니다. 식이 언제 0입니까?

곱해지는 세 부분이 있습니다.

  • (i-j)%9 i와 j가 9의 배수, 즉 동일한 열이면 0입니다.
  • (i/9^j/9) i / 9 == j / 9, 즉 동일한 행이면 0입니다.
  • (i/27^j/27|i%9/3^j%9/3) 둘 다 0이면 0입니다.
    • i/27^j^27 i / 27 == j / 27이면 0, 즉 3 개 행의 동일한 블록
    • i%9/3^j%9/3 i % 9 / 3 == j % 9 / 3 인 경우 0입니다. 즉, 3 개 열의 동일한 블록

이 세 부분 중 하나가 0이면 전체 표현식은 0입니다. 즉, 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
... 정말 열심히 노력하면 파이썬에서 여전히 나쁜 코드를 작성할 수 있음을 보여줍니다. :-)
John Fouhy

2
명확성을 i%9/3 == j%9/3위해 (i%9) / 3 == (j%9) / 3. 작업자의 순서를 마음대로 알아야한다는 것을 알고 있지만 잊기 쉽고 스캔하기가 더 쉽습니다.
Jordan Reiter

1
함수에 전달 된 숫자가 잘못되면 어떻게됩니까? 이것은 영원히 지속 될까요, 아니면 모든 조합이 시도 된 후에 저절로 종료 될까요?
Gundars Mēness 2011

2
@ GundarsMēness 재귀의 각 지점에서 하나의 빈 위치가 처리됩니다. 이 위치에 유효한 숫자를 찾을 수 없으면 함수는 단순히 반환됩니다. 즉, 첫 번째 빈 위치에 대한 유효한 숫자를 찾을 수없는 경우 (즉, 입력이 잘못된 스도쿠 인 경우) 전체 프로그램이 출력없이 반환됩니다 ( sys.exit(a)도달하지 않음)
MartinStettner

5
@JoshBibb 나는 이것이 오래된 게시물이라는 것을 알고 있지만 이것은 Python2 용으로 작성되었으며 Python3에서 실행 중이기 때문에 오류가 발생합니다. 모든 교체 /연산자 same_row, same_col그리고 same_block//연산자를하고 당신은 바로 답을 얻을 수 있습니다.
Adam Smith

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

따라서 내부 목록 표현식을 해결하면됩니다. 줄에 설정된 숫자를 수집한다는 것을 알고 있습니다. 그렇지 않으면 주변 코드가 의미가 없습니다. 그러나 나는 그것이 어떻게하는지에 대한 실제 단서가 없습니다 (그리고 나는 지금 그 바이너리 공상을 해결하기에는 너무 피곤합니다, 죄송합니다)


나는 파이썬 전문가가 아니지만, 라인 3은 또는 나는 당신의 논리가 역으로 생각, 그래서 출구
바비 잭

i = -1이라고 가정합니다. 그러면 ~ i = 0이고 0 또는 foo는 foo가 평가되도록합니다. 반면에 i! = -1이면 ~ i가 0이 아니므로 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 프로그램의 출력은 다음과 같습니다. 817311213622482322131224934443535441555798655266156777774663869988847188399979596

솔루션에 해당하지 않습니다. 사실 여러분은 이미 모순을 볼 수 있습니다. 첫 번째 줄에 2 개의 1이 있습니다.


1
좋은 지적. 그런 퍼즐을 어떻게 찾았습니까? 이 솔버를 던지는 퍼즐에 어떤 종류의 특성이 있습니까?
Ville Salonen 2014

3
주의 : Python 2.7로 작성되었으며 다음과 같은 올바른 응답을 생성합니다. 897451623632987415415236987749325861163798254258164379584613792976542138321879546. 분할이 다르므로 Python 3을 사용하지 마십시오.
베타 프로젝트
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.