web-dev-qa-db-fra.com

Solveur Sudoku le plus court en Python - Comment ça marche?

Je jouais avec mon propre solveur Sudoku et je cherchais des conseils pour une conception bonne et rapide quand je suis tombé sur ceci:

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])

Ma propre implémentation résout Sudokus de la même manière que je les résous dans ma tête, mais comment fonctionne cet algorithme cryptique?

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

76
Ville Salonen

Eh bien, vous pouvez rendre les choses un peu plus faciles en corrigeant la syntaxe:

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])

Nettoyer un peu:

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])

D'accord, ce script attend donc un argument de ligne de commande et appelle la fonction r dessus. S'il n'y a pas de zéros dans cette chaîne, r quitte et affiche son argument.

(Si un autre type d'objet est transmis, Aucun équivaut à passer zéro et tout autre objet est imprimé dans sys.stderr et donne un code de sortie de 1. En particulier, sys.exit ("un message d'erreur") est un moyen rapide de quitter un programme en cas d'erreur. Voir http://www.python.org/doc/2.5.2/lib/module-sys.html )

Je suppose que cela signifie que les zéros correspondent à des espaces ouverts, et un puzzle sans zéros est résolu. Ensuite, il y a cette méchante expression récursive.

La boucle est intéressante: for m in'%d'%5**18

Pourquoi 5 ** 18? Il s'avère que '%d'%5**18 Est évalué à '3814697265625'. Il s'agit d'une chaîne qui contient au moins une fois chaque chiffre de 1 à 9, alors il essaie peut-être de placer chacun d'eux. En fait, il semble que c'est ce que fait r(a[:i]+m+a[i+1:]): appeler récursivement, avec le premier blanc rempli par un chiffre de cette chaîne. Mais cela ne se produit que si l'expression précédente est fausse. Regardons cela:

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)]

Le placement n'est donc effectué que si m n'est pas dans cette liste de monstres. Chaque élément est soit un nombre (si la première expression n'est pas nulle) ou un caractère (si la première expression est zéro). m est exclu comme une substitution possible s'il apparaît comme un caractère, ce qui ne peut se produire que si la première expression est nulle. Quand l'expression est-elle nulle?

Il se décompose en trois parties:

  • (i-j)%9 Qui est nul si i et j sont un multiple de 9, c'est-à-dire la même colonne.
  • (i/9^j/9) Qui est nul si i/9 == j/9, c'est-à-dire la même ligne.
  • (i/27^j/27|i%9/3^j%9/3) Qui est nul si les deux sont nuls:
    • i/27^j^27 Qui est nul si i/27 == j/27, c'est-à-dire le même bloc de trois lignes
    • i%9/3^j%9/3 Qui est nul si i% 9/3 == j% 9/3, c'est-à-dire le même bloc de trois colonnes

Si l'une de ces trois parties est nulle, l'expression entière est nulle. En d'autres termes, si i et j partagent une ligne, une colonne ou un bloc 3x3, la valeur de j ne peut pas être utilisée comme candidate pour le blanc en i. Ah!

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])

Notez que si aucun des emplacements ne fonctionne, r reviendra et reviendra au point où quelque chose d'autre peut être choisi, c'est donc un premier algorithme de profondeur de base.

N'utilisant aucune heuristique, ce n'est pas particulièrement efficace. J'ai pris ce puzzle de Wikipedia ( http://en.wikipedia.org/wiki/Sudok ):

$ time python sudoku.py 530070000600195000098000060800060003400803001700020006060000280000419005000080079
534678912672195348198342567859761423426853791713924856961537284287419635345286179

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

Addendum: Comment je le réécrirais en tant que programmeur de maintenance (cette version a environ une accélération de 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 __== '__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'
215
Bill Barksdale

le désobscurcir:

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

Donc, nous avons juste besoin de travailler sur l'expression de la liste intérieure. Je sais qu'il recueille les chiffres définis dans la ligne - sinon, le code qui l'entoure n'a aucun sens. Cependant, je n'ai aucune idée de comment cela se passe (et je suis trop fatigué pour comprendre cette fantaisie binaire en ce moment, désolé)

9
Tetha

r(a) est une fonction récursive qui tente de remplir un 0 dans le tableau à chaque étape.

i=a.find('0');~i or exit(a) est la terminaison en cas de succès. S'il n'y a plus de valeurs 0 Dans la carte, nous avons terminé.

m est la valeur actuelle avec laquelle nous allons essayer de remplir le 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)] évalue à true s'il est manifestement incorrect de mettre m dans le 0 actuel. Surnommons-le "is_bad". C'est la partie la plus délicate. :)

is_bad or r(a[:i]+m+a[i+1:] Est une étape récursive conditionnelle. Il essaiera récursivement d'évaluer le prochain 0 Dans le tableau si le candidat de la solution actuelle semble sain d'esprit.

for m in '%d'%5**18 Énumère tous les nombres de 1 à 9 (inefficacement).

6
Deestan

De nombreux solveurs de sudoku courts essaient de manière récursive tous les numéros légaux possibles jusqu'à ce qu'ils aient rempli les cellules avec succès. Je ne l'ai pas démonté, mais juste l'écrémer, on dirait que c'est ce qu'il fait.

4
Lou Franco

Le code ne fonctionne pas réellement. Vous pouvez le tester vous-même. Voici un exemple de puzzle sudoku non résolu:

807000003602080000000200900040005001000798000200100070004003000000040108300000506

Vous pouvez utiliser ce site Web ( http://www.sudokuwiki.org/sudoku.htm ), cliquer sur importer le puzzle et copier simplement la chaîne ci-dessus. La sortie du programme python est: 817311213622482322131224934443535441555798655266156777774663869988847188399979596

qui ne correspond pas à la solution. En fait, vous pouvez déjà voir une contradiction, deux 1 dans la première rangée.

1
Basil