Найкоротший вирішувач судоку в 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])

Моя власна реалізація вирішує Sudokus так само, як я їх вирішую в своїй голові, але як працює цей загадковий алгоритм?

http://scottkirkwood.blogspot.com/2006/07/shortest-sudoku-solver-in-python.html


21
це виглядає як участь у затьмареному конкурсі perl! Я думав, що одним із пунктів python було написання чистого коду, який можна було б легко зрозуміти :)
warren

1
Цей python не схожий на свій відступ правильно. : /
Джейк

18
Це ще один доказ того, що ви можете писати незрозумілий код будь-якою мовою.
JesperE

Я думаю, це, мабуть, була кодова відповідь на гольф.
Loren Pechtel

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 ("деяке повідомлення про помилку") є швидкий спосіб виходу з програми при виникненні помилки. Див. 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 повернеться і повернеться назад до точки, де можна вибрати щось інше, тому це базовий алгоритм спочатку глибини.

Не використовуючи жодної евристики, це не особливо ефективно. Я взяв цю головоломку з Вікіпедії ( http://en.wikipedia.org/wiki/Sudoku ):

$ time python sudoku.py 530070000600195000098000060800060003400803001700020006060000280000419005000080079
534678912672195348198342567859761423426853791713924856961537284287419635345286179

real    0m47.881s
user    0m47.223s
sys 0m0.137s

Додаток: Як би я переписав його як програміста з обслуговування (ця версія має швидкість 93x :)

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, якщо дуже стараєтесь :-)
Джон Фухі

2
Просто для явності, можливо, ви захочете змінити i%9/3 == j%9/3на (i%9) / 3 == (j%9) / 3. Я знаю, що ви повинні знати порядок операторів напам'ять, але про це легко забути і полегшити сканування.
Jordan Reiter

1
Що робити, якщо цифри, передані функції, помилкові? Це буде тривати назавжди, або воно припиниться само собою після всіх випробуваних комбінацій?
Gundars Mēness

2
@ GundarsMēness У кожній точці рекурсії обробляється одна порожня позиція. Якщо не вдається знайти дійсну цифру для цієї позиції, функція просто повертається. Це означає, що якщо не вдається знайти дійсну цифру для першої порожньої позиції (тобто sys.exit(a)
введене

5
@JoshBibb Я знаю, що це старий допис, але ця помилка трапляється у вас, оскільки це було написано для Python2, і ви запускаєте його в Python3. Замініть усі /оператори в same_row, same_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

Отже, нам просто потрібно опрацювати внутрішній вираз списку. Я знаю, що він збирає цифри, встановлені в рядку - інакше код навколо нього не має сенсу. Однак я не маю реального уявлення, як це робиться (і я занадто втомлений, щоб виробити цю двійкову фантазію прямо зараз, вибачте)


Я не фахівець з пітонів, але рядок 3 є або вихід, тому я думаю, що ваша логіка зворотна
Боббі Джек

Припустимо, що i = -1. Тоді ~ i = 0, і 0 або foo призводить до оцінки foo. З іншого боку, якщо i! = -1, тоді ~ i буде ненульовим, отже, перша частина або буде істинною, що призводить до того, що другий параметр або НЕ обчислюється через коротке замикання оцінка.
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

що не відповідає рішенню. Насправді ви вже бачите протиріччя, два одиниці 1 у першому ряду.


1
Влучне зауваження. Як вам вдалося знайти таку загадку? Чи є якась характеристика у головоломці, яку кидає цей вирішувач?
Ville Salonen

3
Обережно: він був написаний на Python 2.7 і дав правильну відповідь: 897451623632987415415236987749325861163798254258164379584613792976542138321879546. Не використовуйте Python 3, оскільки розбіжності різні.
Бета-проекти
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.