Sudokus

January 28, 2018 | Author: Aprender Libre | Category: Computer Program, Computer Programming, Areas Of Computer Science, Computing, Technology
Share Embed Donate


Short Description

Descripción: Python es un lenguaje que gusta mucho en la comunidad de Inteligencia Artificial, no en vano Google dice qu...

Description

061-064_PITON_13

02.12.2005

11:14

Uhr

Página

61

Python • DESARROLLO

HAL 9000 contra los Sudokus Mutantes

SUDOKUS

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É MARÍA RUIZ Y JOSE PEDRO ORANTES

¿

Ha sucumbido el lector a los Sudokus? Debo confesar que jamás 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á diversión, pero no es mi caso) no me atrae demasiado.

El Ataque de los Sudokus Asesinos Pero si el verano del año 2005 se recordará por algo, será por el Reageton (o como se escriba) y por los omnipresentes Sudokus. Los hemos visto en los periódicos, en revistas, en bolsas de comestibles… y más de uno ha comprado libros de Sudokus para echar el rato. Tal ha sido la acogida que un día vino mi hermano diciendo que no era capaz de resolver uno. Me pidió consejo, pero le dije que no tenía ni idea de qué iba la cosa. Después de leer sobre el tema pensé que sería buena idea usarlo como base para explicar algunos conceptos de Inteligencia Artificial aplicados a Python.

Búsqueda de soluciones. La Inteligencia Artificial, o como gustan muchos de llamarla «Algoritmos

Ingeniosos», tiene como fin resolver pronúmeros 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 númesus éxitos no han conseguido simular ros de uno al nueve sin repetirse. Así de cerebros reales, Terminator aún 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 aéreo o la planificación, han Backtracking o Vuelta Atrás sido mucho más espectacula(haré uso del nombre en res de lo que se creía. inglés por ser el de más uso) Pero dicho de esa manera para solucionar Sudokus. parece quitársele el glamour, Un mini-Sudoku de El Backtracking. cuando en realidad aún lo 3x3 casillas. tiene. Uno de los campos que Se trata de un algoritmo de más éxito ha tenido ha sido en la creabúsqueda en el cual se explora un árbol ción de algoritmos de búsqueda de solude soluciones en anchura o profundidad. ciones. ¿Recuerda el lector las noticias Dicho de esta manera queda muy técnico 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 búsDigamos 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 están Según la página de Wikipedia, un pasaocupadas pero otras no. En cada hueco tiempo japonés que apareció a mediados solo podemos poner un número comde los años 80 en los diarios nipones y prendido entre el 1 y el 3 de tal manera que en el año 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 números 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 o un

WWW.LINUX- MAGAZINE.ES

Número 13

61

061-064_PITON_13

02.12.2005

11:14

Uhr

Página

62

DESARROLLO • Python

. ¿Donde está el juego? en hallar los números que hay que poner en cada hueco de forma consecutiva de tal manera que se verifique que cada número es único en su fila y su columna. ¿Cómo podríamos elaborar un programa que resuelva este puzzle? Existen varias opciones, pero nos centraremos en la que queremos ver. Comenzaríamos centrándonos en los huecos, por tanto debemos ignorar las celdas que estén ocupadas. Para cada hueco tenemos que elegir un número que no se encuentre ni en su fila ni en su columna. Vamos a necesitar, por tanto, una función que escanee ambas en busca de números no usados. Seleccionaremos uno de los posibles números y avanzaremos hacia la derecha rellenando los huecos hasta llegar al último.

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

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

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 posición de manera sencilla a través de sus métodos. Funcionará como un iterador: primero definimos los límites,

Listado 1: clase «Posicion()». 01 class Posicion: 02 ### Gestiona una posición, 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

62

Número 13

29 30 31 32 33 34 35 36 37 38 39 40

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

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 posición 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]

WWW.LINUX- MAGAZINE.ES

tenemos un método que nos permite avanzar y otros para ajustar los valores o recojerlos. Podemos ver el código 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]

A Posicion le pasaremos el tamaño de las filas y las columnas del Sudoku, que al ser cuadrado serán los mismo.

El Sudoku. Vamos a crear una función que nos permita visualizar el Sudoku. Podemos ver el código en el Listado 2. Pero esta función acepta dos parámetros en lugar de uno ¿por qué? Usaremos una representación algo extraña. En lugar de ir rellenando los huecos sobre el propio Sudoku vamos a ir introduciendo los número que corresponden a los huecos en una PILA donde cada elemento tendrá la forma . La razón de hacerlo así es que nos permitirá trabajar de manera más sencilla durante la búsqueda. En la función imprime esta PILA recibe el nombre de asignadas. En esta función 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.

Buscando Posibilidades Cuando estemos en un hueco, necesitamos generar una lista de números no usados ni en la fila ni en la columna en las que se encuentra. Esa será la tarea de la función prueba que aparece en el Listado 3. Esta función generará una lista con los números sin utilizar. Para hacerlo prueba todos los números 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

061-064_PITON_13

02.12.2005

11:14

Uhr

Página

63

Python • DESARROLLO

eso prueba no solo busca en nuestro Sudoku, sino en esta PILA, para buscar en los números que ya hemos puesto. Por tanto primero escanearemos la fila, después la columna y por último la PILA. Cada número que no aparezca en ninguno de los tres sitios será añadido a una lista que se devuelve al acabar la función.

¿Qué es el Backtracking? Podemos contestar haciendo otra pregunta ¿qué ocurrirá si en un momento dado la función prueba nos devuelve una lista vacía? Eso quiere decir que no se ha encontrado ningún número libre para poder usarlo. ¿Acaba con esto nues-

Listado 2: función «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

básico 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 resolución.

tro intento de Sudoku? ¡Ni mucho menos! ahora comienza lo divertido. Un jugador humano suele tantear, pone un número aquí y otro allí. Intenta ir ajustado «a ojo» la posición de cada número. Nosotros estamos creando un programa, y estos suelen ser muy metódicos y mecánicos en lo que hacen. En palabras técnicas vamos a explorar el espacio de posibilidades. Imagina todas las combinaciones posibles de números que se pueden poner en el Sudoku, pues nosotros vamos a ir recorriéndolas 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 atrás» hasta el último paso donde pudimos elegir entre varios números 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 fácil, pero hacer un programa que haga esto no es tan sencillo como pudiese parecer. Existen varias técnicas y este proceso posee innumerables optimizaciones. Pero nosotros nos quedaremos con el caso básico. Cuando comencé a escribir el programa opté por el enfoque recursivo, que consiste en hacer una función que se llamen a sí misma. Esta manera de hacer el programa requiere menos código 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

WWW.LINUX- MAGAZINE.ES

El truco consiste en emplear dos pilas. Las pilas son estructuras de datos que nos permite guardar información 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 tontería, pero la estructura de pila es vital en casi todos los ámbitos de la informática y es quizás 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 más antiguas al fondo y las más nuevas al frente) y otra para ir almacenando las opciones que no escogimos. Por ejemplo digamos que estamos en la posición [3,4] y que la función prueba nos ha devuelto la lista [3,7,9], entonces las pilas tendrán los valores: >>> pilaActual [...[3,3,2],[3,4,3]] >>> pilaPosibles [...[3,4,7],[3,4,9]]

Supongamos que después 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 elección de [3,4,3] escogiendo ahora otra de las opciones que guardamos. Para ello usaremos la función 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]]

Número 13

63

061-064_PITON_13

02.12.2005

11:14

Uhr

Página

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 código de la función que realiza la búsqueda y el Backtracking aparece en el listado 4 (disponible en [1]. Vamos a analizarla con más detalle.

La función «resuelve()» Lo primero que hacemos es conseguir el tamaño de nuestro Sudoku y preparar las pilas (que son listas vacías) así como una variable que represente la posición. Entonces entramos en un bucle while del que saldremos cuando hayamos llegado al final del Sudoku. En el bucle buscamos todas los posibles números que se pueden usar en un hueco. Si la función prueba() nos devuelve una lista vacía, entonces debemos avanzar a la siguiente posición porque la actual ya tiene un número. Este

bucle es peligroso, porque en él es donde se da la condición que acaba con la función, por eso tenemos un return dentro de él. Comprobamos si prueba nos devuelve [-1], indicativo de que es imposible escoger un número porque todos están ya cogidos. Es aquí donde ejecutamos el Backtracking, pero lo veremos después, sigamos con el else. Una vez que tenemos la lista de posibles números, cogemos el primero y lo almacenamos en pilaActual para poder seguir avanzando. Los otros posibles números se almacenan, junto a la posición del hueco, en la pilaPosibles para su posterior uso si fuese necesario. Es entonces cuando avanzamos a la siguiente posición. Recuperemos el tema del Backtracking, prueba() nos había comunicado que no era posible dar ningún número candidato. Lo primero que hace-

Listado 3: función «prueba()». 01 def prueba(sudoku, asignadas, fila, col): 02 # Puede dar 3 cosas: 03 # a) [] si está ocupado 04 # b) tupla con números 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 números 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

64

Número 13

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

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)

40 41 42

if resultado == []: # Un callejón sin

>>> pilaActual [......[6,1,4],[6,2,8][6,3,9]] >>> pilaPosibles [......[6,1,5]]

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. Sería como haber avanzado hasta el hueco [6,3], volver hasta el [6,1] y arrancar de nuevo. Cuando acabamos de desapilar actualizamos la posición 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 automáticamente… ¿y el rendimiento? Las exploraciones de espacios de soluciones son lentas. No he introducido optimizaciones porque complicarían innecesariamente el código. En mi máquina, 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 página 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 límite. Aún así nuestro código, sin ser el más óptimo, es sin duda de los más sencillos. Como dice el gran maestro Knuth, «la optimización prematura es el origen de todos los males». Lo importante es que el programa sea sencillo, fácil de escribir y correcto. Después siempre hay tiempo ■ para optimizar.

RECURSOS

salida 43 44 45

mos es desapilar con pop() el último estado de pilaActual, porque desde ahí no nos hemos podido mover a la siguiente posición. ¿Por qué hay un bucle while? Como hemos ido introduciendo en una pila las otras posibilidades, las más recientes son las que están 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?

resultado = [-1] return resultado

WWW.LINUX- MAGAZINE.ES

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

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF