You are on page 1of 4

061-064_PITON_13

02.12.2005

11:14

Uhr

Pgina

61

Python DESARROLLO

SUDOKUS

HAL 9000 contra los Sudokus Mutantes

Python es un lenguaje que gusta mucho en la comunidad de Inteligencia Artificial, no en vano Google dice que es una de sus armas. Hoy vamos a convertirlo en la nuestra. POR JOS MARA RUIZ Y JOSE PEDRO ORANTES

Ha sucumbido el lector a los Sudokus? Debo confesar que jams he hecho uno. Soy una persona algo vaga, debo reconocerlo, y la idea de estar mucho tiempo delante de un problema que no me reporta nada (alguno dir diversin, pero no es mi caso) no me atrae demasiado.

El Ataque de los Sudokus Asesinos


Pero si el verano del ao 2005 se recordar por algo, ser por el Reageton (o como se escriba) y por los omnipresentes Sudokus. Los hemos visto en los peridicos, en revistas, en bolsas de comestibles y ms de uno ha comprado libros de Sudokus para echar el rato. Tal ha sido la acogida que un da vino mi hermano diciendo que no era capaz de resolver uno. Me pidi consejo, pero le dije que no tena ni idea de qu iba la cosa. Despus de leer sobre el tema pens que sera buena idea usarlo como base para explicar algunos conceptos de Inteligencia Artificial aplicados a Python.

Bsqueda de soluciones.
La Inteligencia Artificial, o como gustan muchos de llamarla Algoritmos

Ingeniosos, tiene como fin resolver pronmeros y el resto huecos. Se tienen que blemas complejos. El lector puede dorrellenar los huecos de manera que en mir tranquilo, porque por el momento cada fila y columna aparezcan los nmesus xitos no han conseguido simular ros de uno al nueve sin repetirse. As de cerebros reales, Terminator an no est simple y as de complicado. cerca. En cambio, los xitos Nosotros vamos a ver, y a cosechados en problemas implementar, un algoritmo complicados, como el control bastante famoso: el areo o la planificacin, han Backtracking o Vuelta Atrs sido mucho ms espectacula(har uso del nombre en res de lo que se crea. ingls por ser el de ms uso) Pero dicho de esa manera para solucionar Sudokus. parece quitrsele el glamour, Un mini-Sudoku de El Backtracking. cuando en realidad an lo 3x3 casillas. tiene. Uno de los campos que Se trata de un algoritmo de ms xito ha tenido ha sido en la creabsqueda en el cual se explora un rbol cin de algoritmos de bsqueda de solude soluciones en anchura o profundidad. ciones. Recuerda el lector las noticias Dicho de esta manera queda muy tcnico sobre ordenadores que derrotan a granas que mejor vemos un ejemplo con un des maestros de ajedrez? Pues su xito se mini-sudoku. debe a sofisticados algoritmos de bsDigamos que tenemos el siguiente proqueda de soluciones. blema: se nos presenta un cuadro como el de la Figura 1, tres filas por tres Qu es un Sudoku? columnas, donde algunas de ellas estn Segn la pgina de Wikipedia, un pasaocupadas pero otras no. En cada hueco tiempo japons que apareci a mediados solo podemos poner un nmero comde los aos 80 en los diarios nipones y prendido entre el 1 y el 3 de tal manera que en el ao 2005 ha arrasado en los del que en cada fila y en cada columna solo resto del mundo. El jugador tiene una aparezca cada uno de estos nmeros una matriz de nueve filas por nueve columvez. Por tanto es imposible encontrar nas de las cuales algunas celdas tienen una fila que muestre un <1,3,3> o un

WWW.LINUX- MAGAZINE.ES

Nmero 13

61

061-064_PITON_13

02.12.2005

11:14

Uhr

Pgina

62

DESARROLLO Python

<2,1,1>. Donde est el juego? en hallar los nmeros que hay que poner en cada hueco de forma consecutiva de tal manera que se verifique que cada nmero es nico en su fila y su columna. Cmo podramos elaborar un programa que resuelva este puzzle? Existen varias opciones, pero nos centraremos en la que queremos ver. Comenzaramos centrndonos en los huecos, por tanto debemos ignorar las celdas que estn ocupadas. Para cada hueco tenemos que elegir un nmero que no se encuentre ni en su fila ni en su columna. Vamos a necesitar, por tanto, una funcin que escanee ambas en busca de nmeros no usados. Seleccionaremos uno de los posibles nmeros y avanzaremos hacia la derecha rellenando los huecos hasta llegar al ltimo.

corresponder con una lista, por tanto un sudoku de tres filas por tres columnas podr ser:
>>> sudoku = [[1,0,0], [0,2,0], [0,0,3]] >>> sudoku[0][0] 1 >>> sudoku[1][2] 0

tenemos un mtodo que nos permite avanzar y otros para ajustar los valores o recojerlos. Podemos ver el cdigo en el Listado 1. Un ejemplo de uso es:
01 02 03 04 05 06 07 08 09 10 11 12 13 >>> p = Posicion(1,1) >>> p.getPos() [0,0] >>> p.sig() >>> p.sig() >>> p.getPos() [1,0] >>> p.sig() >>> p.sig() >>> p.fin() true >>> p.getPos() [-1,-1]

Estructura de datos
No vamos a complicar mucho el diseo, usaremos una lista de listas. Cada fila se

Ahora ya podemos trabajar sobre una estructura de datos. Los huecos los vamos a representar como 0s. Cuando nos movemos por el Sudoku tenemos que tener en cuenta cuando nos hemos salido de la fila y tenemos que seguir en la siguiente. Para ello vamos a emplear una clase que llamaremos Posicion. Esta clase nos va a permitir controlar la posicin de manera sencilla a travs de sus mtodos. Funcionar como un iterador: primero definimos los lmites,

A Posicion le pasaremos el tamao de las filas y las columnas del Sudoku, que al ser cuadrado sern los mismo.

El Sudoku.
Vamos a crear una funcin que nos permita visualizar el Sudoku. Podemos ver el cdigo en el Listado 2. Pero esta funcin acepta dos parmetros en lugar de uno por qu? Usaremos una representacin algo extraa. En lugar de ir rellenando los huecos sobre el propio Sudoku vamos a ir introduciendo los nmero que corresponden a los huecos en una PILA donde cada elemento tendr la forma <FILA,COLUMNA,VALOR>. La razn de hacerlo as es que nos permitir trabajar de manera ms sencilla durante la bsqueda. En la funcin imprime esta PILA recibe el nombre de asignadas. En esta funcin generamos barras de guiones y recorremos el Sudoku imprimiendo las filas hasta completar un tablero. Se puede observar el resultado final en la Figura 2.

Listado 1: clase Posicion().


01 class Posicion: 02 ### Gestiona una posicin, controlando los incrementos 03 ### 04 def __init__(self,maxfila,maxcol): 05 self.maxfila = maxfila 06 self.maxcol = maxcol 07 self.fila = 0 08 self.col = 0 09 10 def setFila(self, fila): 11 if fila < 0: 12 self.fila = 0 13 elif fila >= self.maxfila: 14 self.fila = -1 15 else: 16 self.fila = fila 17 18 def setCol(self, col): 19 if col < 0: 20 self.col = 0 21 elif col >= self.maxcol: 22 self.col = -1 23 else: 24 self.col = col 25 26 def getFila(self): 27 return self.fila 28 29 30 31 32 33 34 35 36 37 38 39 40 def getCol(self): return self.col def fin(self): return self.fila == -1 and self.col == -1 def reset(self): self.fila = 0 self.col = 0 def sig(self): # Incrementa la posicin controlando que no se pasa # del final. if not self.fin(): self.col += 1 if self.col == self.maxcol: self.col = 0 self.fila +=1 if self.fila == self.maxfila: self.fila = -1 self.col = -1 def getPos(self): return [self.fila, self.col]

41 42 43 44 45 46 47 48 49 50 51 52

Buscando Posibilidades
Cuando estemos en un hueco, necesitamos generar una lista de nmeros no usados ni en la fila ni en la columna en las que se encuentra. Esa ser la tarea de la funcin prueba que aparece en el Listado 3. Esta funcin generar una lista con los nmeros sin utilizar. Para hacerlo prueba todos los nmeros posibles tanto en la fila como en la columna. Si no encuentra ninguno disponible devuelve una lista [-1]. Como ya dijimos antes, guardaremos los huecos rellenos en una PILA, y por

62

Nmero 13

WWW.LINUX- MAGAZINE.ES

061-064_PITON_13

02.12.2005

11:14

Uhr

Pgina

63

Python DESARROLLO

eso prueba no solo busca en nuestro Sudoku, sino en esta PILA, para buscar en los nmeros que ya hemos puesto. Por tanto primero escanearemos la fila, despus la columna y por ltimo la PILA. Cada nmero que no aparezca en ninguno de los tres sitios ser aadido a una lista que se devuelve al acabar la funcin.

bsico consiste en usar bucles, es menos elegante pero no explota :). Pero como vamos a guardar tanto la ruta como las opciones que no escogimos? Es hora de desvelar el misterio.

Las dos pilas


Mostramos un Sudoku para su resolucin.

Qu es el Backtracking?
Podemos contestar haciendo otra pregunta qu ocurrir si en un momento dado la funcin prueba nos devuelve una lista vaca? Eso quiere decir que no se ha encontrado ningn nmero libre para poder usarlo. Acaba con esto nues-

Listado 2: funcin imprime().


01 def imprime(sudoku, asignadas): 02 barra = "" 03 for i in range(0,COLUMNAS): 04 barra += "---" 05 barra += "." 06 07 print barra 08 for fila in range(0,len(sudoku)): 09 cadena = "" 10 for columna in range(0,len(sudoku)): 11 if sudoku[fila][columna] == 0: 12 encontrado = False 13 for a in asignadas: 14 if a[0] == fila and a[1] == columna: 15 cadena += "| "+ str(a[2]) + " " 16 encontrado = True 17 if not encontrado: 18 cadena += "|"+" " 19 else: 20 cadena += "| "+str(sudoku[fila][columna])+" " 21 cadena += "|" 22 print cadena 23 print barra

tro intento de Sudoku? Ni mucho menos! ahora comienza lo divertido. Un jugador humano suele tantear, pone un nmero aqu y otro all. Intenta ir ajustado a ojo la posicin de cada nmero. Nosotros estamos creando un programa, y estos suelen ser muy metdicos y mecnicos en lo que hacen. En palabras tcnicas vamos a explorar el espacio de posibilidades. Imagina todas las combinaciones posibles de nmeros que se pueden poner en el Sudoku, pues nosotros vamos a ir recorrindolas paso a paso. Pero no lo vamos a hacer de manera tan bruta, sino que iremos comprobando hasta donde podemos llegar con cada una y cuando nos atasquemos, volveremos atrs hasta el ltimo paso donde pudimos elegir entre varios nmeros y cogeremos el siguiente al que cogimos la ltima vez. Si llegamos al hueco [1,3] y podemos elegir entre el 1, el 3 y el 5; escogeremos el primero, el 1, y nos guardaremos el 3 y el 5 por si los necesitamos. Avanzaremos tanto como podamos y cuando no podamos seguir, porque sea imposible, iremos deshaciendo el camino hecho y escogiendo en cada paso las otras posibilidades. Dicho as parece intuitivo y hasta fcil, pero hacer un programa que haga esto no es tan sencillo como pudiese parecer. Existen varias tcnicas y este proceso posee innumerables optimizaciones. Pero nosotros nos quedaremos con el caso bsico. Cuando comenc a escribir el programa opt por el enfoque recursivo, que consiste en hacer una funcin que se llamen a s misma. Esta manera de hacer el programa requiere menos cdigo pero en cuanto el Sudoku crece el programa deja de funcionar debido a que satura la pila del sistema. Esto ocurre cuando se invoca a muchas funciones, unas dentro de otras, y con problemas de este tipo podemos tener cientos o miles de invocaciones con lo que, tarde o temprano, el programa dar un error. El otro enfoque

El truco consiste en emplear dos pilas. Las pilas son estructuras de datos que nos permite guardar informacin y recuperarla posteriormente de una manera peculiar: lo ltimo que almacenemos ser lo primero que podamos guardar. Es como una pila de platos, puedes ponerlos unos encima de otros, pero es complicado sacarlos desde abajo as que se sacan en orden inverso a como se pusieron. Puede parecer una tontera, pero la estructura de pila es vital en casi todos los mbitos de la informtica y es quizs uno de sus mayores descubrimientos. Su secreto consiste en aprovechar su manera de funcionar. Haremos lo siguiente: tendremos dos pilas, una para guardar las elecciones hechas hasta el momento (con las ms antiguas al fondo y las ms nuevas al frente) y otra para ir almacenando las opciones que no escogimos. Por ejemplo digamos que estamos en la posicin [3,4] y que la funcin prueba nos ha devuelto la lista [3,7,9], entonces las pilas tendrn los valores:
>>> pilaActual [...[3,3,2],[3,4,3]] >>> pilaPosibles [...[3,4,7],[3,4,9]]

Supongamos que despus de una serie de malas elecciones hemos llegado hasta [3,4,3] y ninguna de las elecciones posteriores nos ha valido. Debemos retractarnos de nuestra eleccin de [3,4,3] escogiendo ahora otra de las opciones que guardamos. Para ello usaremos la funcin pop(), que extrae el ltimo elemento de una lista:
>>> pilaActual.pop() [3,4,3] >>> pilaActual [...[3,3,2]] >>> pilaActual.appendU (pilaPosibles.pop()) [...[3,3,2],[3,4,7]] >>> pilaPosibles [...[3,4,9]]

WWW.LINUX- MAGAZINE.ES

Nmero 13

63

061-064_PITON_13

02.12.2005

11:14

Uhr

Pgina

64

DESARROLLO Python

Hemos sacado [3,4,3] de pilaActual y lo hemos reemplazado por [3,4,7] que sacamos de pilaPosibles. Eso ha sido un Backtracking. El cdigo de la funcin que realiza la bsqueda y el Backtracking aparece en el listado 4 (disponible en [1]. Vamos a analizarla con ms detalle.

La funcin resuelve()
Lo primero que hacemos es conseguir el tamao de nuestro Sudoku y preparar las pilas (que son listas vacas) as como una variable que represente la posicin. Entonces entramos en un bucle while del que saldremos cuando hayamos llegado al final del Sudoku. En el bucle buscamos todas los posibles nmeros que se pueden usar en un hueco. Si la funcin prueba() nos devuelve una lista vaca, entonces debemos avanzar a la siguiente posicin porque la actual ya tiene un nmero. Este

bucle es peligroso, porque en l es donde se da la condicin que acaba con la funcin, por eso tenemos un return dentro de l. Comprobamos si prueba nos devuelve [-1], indicativo de que es imposible escoger un nmero porque todos estn ya cogidos. Es aqu donde ejecutamos el Backtracking, pero lo veremos despus, sigamos con el else. Una vez que tenemos la lista de posibles nmeros, cogemos el primero y lo almacenamos en pilaActual para poder seguir avanzando. Los otros posibles nmeros se almacenan, junto a la posicin del hueco, en la pilaPosibles para su posterior uso si fuese necesario. Es entonces cuando avanzamos a la siguiente posicin. Recuperemos el tema del Backtracking, prueba() nos haba comunicado que no era posible dar ningn nmero candidato. Lo primero que hace-

mos es desapilar con pop() el ltimo estado de pilaActual, porque desde ah no nos hemos podido mover a la siguiente posicin. Por qu hay un bucle while? Como hemos ido introduciendo en una pila las otras posibilidades, las ms recientes son las que estn al final de la pila. Si las coordenadas de los estados que vamos sacando de pilaActual no coinciden con las de pilaPosibles qu puede significa eso?
>>> pilaActual [......[6,1,4],[6,2,8][6,3,9]] >>> pilaPosibles [......[6,1,5]]

Listado 3: funcin prueba().


01 def prueba(sudoku, asignadas, fila, col): 02 # Puede dar 3 cosas: 03 # a) [] si est ocupado 04 # b) tupla con nmeros posibles 05 # c) [-1] si es imposible 06 07 if sudoku[fila][col] != 0: 08 return [] 09 10 else: 11 resultado = [] 12 13 # probamos todos los nmeros posibles 14 for n in range(1,LINEAS+1): 15 16 existe = False 17 18 # barremos lineas 19 for l in range(0,LINEAS): 20 if sudoku[l][col] == n: 21 existe = True 22 break 23 24 # barremos columnas 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 salida 43 44 45 resultado = [-1] return resultado for c in range(0,COLUMNAS): if sudoku[fila][c] == n: existe = True break # Buscamos en las posiciones asignadas for asig in asignadas: if asig[0] == fila and asig[2] == n: existe = True break if asig[1] == col and asig[2] == n: existe = True break if not existe: resultado.append(n) if resultado == []: # Un callejn sin

Pues que no existen alternativa para [6,2,8] ni [6,3,9], lo nico que podemos hacer es desapilar estados de pilaActual hasta encontrar uno que coincida en coordenadas con el ltimo de pilaPosibles. Sera como haber avanzado hasta el hueco [6,3], volver hasta el [6,1] y arrancar de nuevo. Cuando acabamos de desapilar actualizamos la posicin a la de [6,1] y la avanzamos, porque ahora buscaremos cubrir el hueco [6,2]. Y ya est! Es algo complejo, pero ahora podemos resolver Sudokus automticamente y el rendimiento? Las exploraciones de espacios de soluciones son lentas. No he introducido optimizaciones porque complicaran innecesariamente el cdigo. En mi mquina, 1600Mhz, tarda alrededor de 10 minutos en resolver el Sudoku. Esto se debe a que Python no es tan eficiente como otros lenguajes. En la pgina de Wikipedia acerca de los Sudokus se dice que los algoritmos que los resuelven de manera eficiente tardan entorno a dos minutos, as que el lector se puede hacer una idea de donde est el lmite. An as nuestro cdigo, sin ser el ms ptimo, es sin duda de los ms sencillos. Como dice el gran maestro Knuth, la optimizacin prematura es el origen de todos los males. Lo importante es que el programa sea sencillo, fcil de escribir y correcto. Despus siempre hay tiempo I para optimizar.

RECURSOS
[1] Listado completo de este artculo en http://www.linux-magazine.es /Magazine/Downloads/13

64

Nmero 13

WWW.LINUX- MAGAZINE.ES

You might also like