Resumen
Este artículo se enfoca en la aplicación de la programación lineal en
la resolución de Sudokus. Comenzaremos por establecer una sólida base
teórica, explorando los principios fundamentales de la programación
lineal y su relevancia en la solución de problemas lógicos. A medida que
avanzamos en el árticulo, pondremos en práctica estos conceptos mediante
la implementación en Python con la ayuda del paquete PuLP.
Abordaremos dos tipos de Sudokus: el Sudoku estándar y una variante con
reglas adicionales para las diagonales. A lo largo de este proyecto,
desvelaremos cómo estos métodos matemáticos pueden ser una herramienta
efectiva para abordar rompecabezas de lógica.
Introducción
Motivación
La resolución de Sudokus, como un desafío intelectual de lógica y números, ha intrigado a entusiastas de los rompecabezas y ha encontrado aplicaciones en diversas disciplinas, incluyendo la optimización y la toma de decisiones. Surge una interrogante relevante: ¿cómo abordar la resolución de Sudokus de manera sistemática y eficiente, aprovechando principios matemáticos sólidos?
La programación lineal se presenta como un enfoque metodológico de gran utilidad para enfrentar este desafío. A través de su base teórica sólida y herramientas de optimización, se puede transformar la resolución de Sudokus en un problema matemáticamente bien estructurado, propenso a soluciones efectivas. El presente artículo se enfoca en la exploración de la aplicabilidad de la programación lineal en el contexto de la resolución de Sudokus, demostrando cómo esta metodología puede aportar claridad y eficiencia al proceso. Asimismo, se considera la implementación de estos enfoques en el lenguaje de programación Python, haciendo uso de la biblioteca PuLP para simplificar la implementación práctica.
Objetivos
El propósito central de este artículo radica en presentar una
metodología rigurosa y efectiva para la resolución de Sudokus mediante
programación lineal, aprovechando la biblioteca PuLP en un
entorno de Python. Con el fin de lograrlo, se plantean los siguientes
objetivos específicos.
- Exploración de Fundamentos Teóricos: Se inicia con un repaso de los principios fundamentales de la programación lineal y su relevancia en la resolución de problemas lógicos. Este análisis sólido establece una base conceptual que respalda la aplicación práctica de la programación lineal a la resolución de Sudokus.
- Implementacion práctica en Python: Se detalla la
implementación de los conceptos de programación lineal en el lenguaje de
programación Python, haciendo uso de la biblioteca
PuLP. Este paso posibilita la traducción efectiva de la teoría en soluciones prácticas para la resolución de Sudokus. - Resolución de dos variantes de Sudokus: El artículo se enfoca en la aplicación de la metodología de programación lineal para resolver dos tipos de Sudoku: el Sudoku estándar y una variante en la que, además de las reglas usuales, cada diagonal debe contener cada numero del \(1\) al \(9\) una vez. Este enfoque permite la evaluación de la versatilidad de la metodología en diferentes contextos de resolución de Sudokus.
Marco Teórico
Fundamentos del Sudoku
Ejemplo de un Sudoku y su solución
El Sudoku es un rompecabezas de lógica que se originó en los Estados Unidos bajo el nombre “Colocar Números” en la revista Dell Pencil Puzzles & Word Games. Fue creado por Howard Garns, un arquitecto que se convirtió en un prolífico creador de rompecabezas en su retiro. En la década de 1980, el juego ganó popularidad en Japón y pasó a llamarse “Sudoku,” derivado de “Suji wa dokushin ni kagiru,” que significa “Los dígitos deben permanecer solos.” El Sudoku se dio a conocer internacionalmente en 2005 cuando numerosos periódicos comenzaron a incluirlo en sus pasatiempos.
Estructura y Reglas
Estructura de un Sudoku
Un Sudoku se juega en una cuadrícula de 9x9 celdas dividida en subcuadrículas de 3x3. El objetivo es rellenar las celdas de manera que en cada fila, columna y región se encuentren los números del 1 al 9, sin repetir ninguno de ellos. Un Sudoku bien planteado tiene una solución única y debe tener al menos 17 pistas iniciales para garantizar esta unicidad (Demostración hecha por Gary McGuire, Bastian Tugemann y Gilles Civario en 2012). La solución siempre es un “cuadrado latino,” donde cada número aparece solo una vez en cada fila y columna, con la restricción adicional de no repetir el mismo número en una subcuadrícula.
Técnicas de Solución
Resolver un Sudoku requiere una combinación de técnicas lógicas y deductivas, que incluyen procesos de rastreo, marcado y análisis. Los programas informáticos que resuelven Sudokus pueden estimar la dificultad de un Sudoku basándose en la complejidad de las técnicas de resolución necesarias, lo que permite ofrecer Sudokus de diferentes niveles de dificultad.
Algoritmos de solución.
Los algoritmos de resolución de Sudokus son utilizados tanto por humanos como por programas informáticos. Los métodos computacionales pueden estimar la dificultad de un Sudoku, y existen varios algoritmos, como el Backtracking y la programación lineal de enteros, que utilizan enfoques diferentes para encontrar soluciones. La investigación continua en busca de mejores algoritmos y técnicas de resolución, incluyendo métodos heurísticos y estrategias basadas en deducciones lógicas.
Programación Lineal (LP)
La Programación Lineal (LP), a menudo denominada optimización lineal, es un método poderoso y versátil para encontrar soluciones óptimas en modelos matemáticos sujetos a restricciones expresadas en forma de relaciones lineales. Se enmarca dentro de la optimización matemática y es especialmente adecuada para abordar problemas en los que se busca maximizar ganancias o minimizar costos. La característica distintiva de la programación lineal es su capacidad para trabajar con relaciones lineales tanto en la función objetivo como en las restricciones del problema.
Objetivos de la Programación Lineal
En esencia, un problema de programación lineal busca optimizar una función objetivo, sujeta a restricciones representadas por igualdades y desigualdades lineales. El conjunto de soluciones posibles, conocido como la “región factible,” se describe mediante un polytopo convexo, que es la intersección de espacios finitos, cada uno definido por una desigualdad lineal. La función objetivo es lineal y toma valores reales sobre este polyhedro. Un algoritmo de programación lineal tiene como objetivo encontrar un punto dentro de este polyhedro donde la función objetivo alcance su valor más pequeño o más grande, según el caso.
Estructura de un problema LP
Un problema de programación lineal se formula comunmente de la siguiente manera:
\[\begin{equation} \begin{aligned} \text{Maximizar } \quad & \mathbf{c}^\intercal \mathbf{x} \\ \text{Sujeto a }\quad & \begin{array}{c} \mathbf{A} \mathbf{x} \leq b \\ \mathbf{x} \geq 0 \end{array} \end{aligned} \end{equation}\]
Nota: Aquí el problema dice “Maximizar”, si el objetivo fuese minimizar deberá decir “Minimizar” en su lugar.
Esta formulación tiene 3 componentes principales:
- \(\mathbf{c}^\intercal \mathbf{x}\) es la función objetivo cuyo valor se busca minimizar o maximizar segun el problema que se desea resolver. Aquí \(\mathbf{x}\) y \(\mathbf{c}\) son vectores tal que \(\mathbf{c}\) es dado.
- \(\mathbf{x}\) es el vector que contiene las variables de decisión, aquellas que indican cuanto se debe producir, transportar, comprar, vender etc. Estas son las que indican el resultado final.
- \(\mathbf{A} \mathbf{x} \leq b\) son las restricciones o limitaciones puestas sobre las variables de decisión. Aqui \(\mathbf{A}\) y \(b\) son una matriz y un vector dados respectivamente. Estas restricciones delimitan el resultado del problema.
- Finalmente la restriccion \(\mathbf{x} \geq 0\) es la restricción de no negatividad, la cual se agrega puesto que generalmente estos problemas trabajan con objetos reales.
Tipos de soluciones.
Como ya se habia mencionado, el objetivo de la programacion lineal es encontrar el conjunto óptimo de variables de decision para la función objetivo dada con todas las restricciones aplicadas. Estas soluciones pueden ser factibles u óptimas.
- Una solución factible es una solución que satisface todas las restricciones. Si una solución factible es además uno de los vertices de la región delimitada por las restricciones entonces se dice que es solución básica factible (sbf).
- Una solución factible óptima es aquella que además de satisfacer todas las restricciones da el mejor valor para la función objetivo (ya sea el minimo en caso de minimizar, o el maximo en caso de maximizar).
Teorema Fundamental de la Programación Lineal.
El teorema de fundamental de la PL indica lo siguiente:
Para un problema de programación lineal en su forma estándar con una matriz que satisface hipótesis de rango completo se tiene:
- Si el problema no tiene solución optima \(\implies\) el problema es no acotado o infactible.
- Si hay una solución factible \(\implies\) hay una solución básica factible (sbf).
- Si hay una solución factible óptima \(\implies\) hay una sbf óptima.
El primer enunciado implica que un problema PL que este acotado y sea factible siempre va a tener solución óptima. Por otro lado los enunciados 2 y 3 implican que, si hay una solución factible, es decir si existe una región factible delimitada por las restricciones, entonces va a existir una solución factible que esté ubicada en alguno de los vertices de la región y que si alguna solución es además optima, entonces esa solución también será uno de estos vertices.
Aplicaciones de la Programación lineal.
La programación lineal tiene una amplia gama de aplicaciones en diversas disciplinas, que van desde las matemáticas puras hasta los campos de negocios, economía e ingeniería. Es ampliamente utilizada para resolver problemas de asignación de recursos, planificación logística, optimización de costos y muchos otros escenarios donde se busca optimizar una función sujeta a restricciones lineales.
Programación Lineal de Enteros (ILP) y Programación Lineal Binaria (BILP)
La Programación Lineal de Enteros (ILP, por sus siglas en inglés, Integer Linear Programming) y la Programación Lineal Binaria de Enteros (BILP, por sus siglas en inglés, Binary Integer Linear Programming) son extensiones de la Programación Lineal (PL) que añaden un nivel adicional de complejidad al considerar que algunas o todas las variables de decisión deben ser números enteros (ILP) o restricciones binarias, es decir, toman únicamente los valores \(0\) o \(1\) (BILP). Estas extensiones de PL surgen en situaciones en las que las soluciones óptimas se expresan más naturalmente como números enteros o como decisiones binarias.
Aplicaciones de modelos ILP y BILP.
Los problemas de ILP y BILP tienen aplicaciones en una amplia variedad de campos, como la planificación logística, el diseño de redes, la programación de horarios y la asignación de recursos. La naturaleza discreta de estas variables hace que sean adecuadas para modelar decisiones discretas, como la asignación de personal, la programación de máquinas y la selección de ubicaciones óptimas.
En el caso del proyecto, se usa un modelo BILP ya que, como se verá más adelante, se pueden definir las variables de decisión de forma binaria tal que sean 1 si existe un número en una casilla y 0 si no existe ningún número. Además, es un problema ILP pues es necesario que la solución al sudoku sean números enteros.
Metodos para resolver ILPs
Resolver un problema de ILP o BILP es computacionalmente más desafiante que resolver un problema de PL, ya que la integralidad o la restricción binaria agrega complejidad. Los métodos clásicos para resolver problemas ILP incluyen el Método de Ramificación y Acotamiento, que divide el espacio de soluciones en subconjuntos, y el Método de Planos de Corte, que agregan restricciones para mejorar la búsqueda. Ambos métodos consisten en la “relajación” del problema ILP, es decir, primero se soluciona el problema PL ignorando la restricción de que las variables de decisión sean enteros y luego se resuelve el problema usando esa solución. A continuación se ilustran ambos métodos.
Método de Ramificación y Acotamiento
Es un método ampliamente utilizado en la práctica que permite encontrar soluciones óptimas a problemas ILP. La idea es ir subdividiendo el espacio de soluciones (ramificando) y acotando el valor hasta que se haya explorado el espacio y encontrado una solución optima.
La relajación de la(s) restricción(es) entera(s) RLP (Relaxed Linear Program) de un ILP da una cota superior para el valor de la función objetivo. Ya que la región factible del ILP es un subconjunto del RLP.
Supongase el siguiente problema ILP
\[\begin{equation} \begin{aligned} \text{Maximizar } \quad & z = 3x_1 + 2x_2 \\ \text{Sujeto a }\quad & \begin{array}{c} x_1 + x_2 \leq 6 \\ x_1, x_2 \geq 0 \textbf{ enteros} \end{array} \end{aligned} \end{equation}\]
Si la solución al problema relajado RLP \[\begin{equation} \begin{aligned} \text{Maximizar } \quad & z = 3x_1 + 2x_2 \\ \text{Sujeto a }\quad & \begin{array}{c} x_1 + x_2 \leq 6 \\ x_1, x_2 \geq 0 \end{array} \end{aligned} \end{equation}\]
es entera, entonces es una solución óptima para el ILP. Si no es entera, entonces la solución del RLP es una cota superior para el ILP.
Ahora consideremos el siguiente problema:
\[\begin{equation} \begin{aligned} \text{Maximizar } \quad & z = 8x_1 + 5x_2 \\ \text{Sujeto a }\quad & \begin{array}{c} x_1 + x_2 \leq 6 \\ 9x_1 + 5x_2 \leq 45 \\ x_1, x_2 \geq 0 \textbf{ enteros} \end{array} \end{aligned} \end{equation}\]
La idea es dividir el espacio de soluciones factibles en \(x_1\geq 4\) y \(x_1\leq 3\) para obtener subproblemas que se pueden acotar con su RLP hasta encontrar una solución optima.
A continuación se muestra un diagrama que ejemplifica esta
ramificación y acotamiento.
Método de Planos de Corte.
Este método es una alternativa a Ramificación y Acotamiento con algunas similitudes en cuanto a la delimitación de la región factible. La idea es agregar restricciones (planos de corte) que contengan (satisfagan) todas las soluciones enteras del problema original, pero que sean infringidas por una solución relajada.
El algorítmo es el siguiente:
- Resolver el RLP del ILP
- Si la solución es entera ya es óptima
- Si no, entonces se agregan las restricciones que contengan todas las soluciones enteras del problema original pero que sean infringidas por la solución obtenida.
- Repetir desde el paso 1.
Planteamiento del Problema
El planteamiento de un problema de programación lineal que permita resolver un Sudoku implica dos etapas cruciales: comprender la relación inherente entre el Sudoku y la Programación lineal y luego modelar el problema en forma estándar como se explicó en el marco teórico.
Relación entre el Sudoku y la Programación Lineal.
El Sudoku puede analizarse como un problema de Programación Lineal Binaria (BILP) debido a la naturaleza estructurada y reglamentada del juego. A continuación se listan las razones clave de esta relación:
- Reglas Claras y Restricciones Lineales: El Sudoku sigue reglas claras y simples que se pueden traducir fácilmente en restricciones lineales. Las reglas básicas del Sudoku incluyen que cada fila, columna y submatriz de 3x3 debe contener los números del 1 al 9 sin repetición. Estas restricciones se expresan como ecuaciones lineales que definen la relación entre las variables, es decir, las celdas del tablero.
- Variables de Decisión Binarias: Cada celda del sudoku puede considerarse una variable de decisión binaria (\(0\) o \(1\)). Esta variable \(x_{i,j,k}\) indica si el número \(k\) está asignado a la celda en la fila \(i\) y la columna \(j\). En un modelo BILP, estas variables binarias representan la toma de decisiones sobre qué números se colocan en qué celdas.
- Factibilidad en Lugar de Optimización: A diferencia de problemas de Programación Lineal convencionales, donde el objetivo es optimizar una función, en el Sudoku BILP, el objetivo principal es encontrar una solución factible, es decir, una asignación de números que cumple con todas las restricciones impuestas por las reglas del juego. La función objetivo en el Sudoku BILP no desempeña un papel crítico, ya que cualquier solución factible es igualmente válida.
- Simplicidad Matemática: A pesar de la aparente complejidad del Sudoku, su estructura se puede simplificar en términos matemáticos. La capacidad de expresar las reglas del juego y las restricciones lineales en un formato BILP permite una solución eficiente a través de técnicas de programación lineal entera.
- Adaptabilidad a Variantes: El enfoque BILP es adaptable y puede aplicarse a variantes del Sudoku, como el Sudoku diagonal o Sudoku Irregular, simplemente modificando las restricciones o añadiendo nuevas restricciones según las nuevas reglas del juego. Esto lo hace un enfoque versátil para abordar una variedad de problemas similares al Sudoku.
El Sudoku como modelo BILP
Para definir formalmente el problema, el Sudoku se modela como un BILP. A continuación se definen matemáticamente las variables de decisión y las restricciones. Como ya se mencionó anteriormente, no es necesario definir una función objetivo, ya que solo se busca una solución factible y no es necesaria una solución óptima. Por esto, se define la función objetivo como una constante \(0\).
Variables de Decisión.
Como ya se mencionó anteriormente, se definen las variables de decisión como \[\begin{equation*} x_{i, j, k}=\begin{cases} 1 \quad &\text{Si} \, \text{el numero } k \text{ está en la fila } i \text{, columna } j\\ 0 \quad &\text{} \, \text{de otro modo} \\ \end{cases} \end{equation*}\]
Asi pues, se tienen un total de \(9^3 = 729\) variables de decisión que indican si la existencia del valor o número en la casilla es verdadero o falso. Por ejemplo, \(x_{5, 4, 6} = 1\) indicaría que el valor \(5\) está presente en el cuadro situado en la fila \(4\), columna \(6\). Similarmente si \(x_{5, 4, 6} = 0\), entonces no hay un \(5\) en esa casilla.
Restricciones.
Las restricciones son simplemente las reglas del Sudoku traducidas a lenguaje matemático. La primera regla indica que “cada casilla debe contener un numero” entonces podemos escribir:
\[\sum_{k=1}^{9} x_{i, j, k} = 1 \text{ para } i = 1\text{, ..., }9\text{, } j = 1\text{, ..., }9\]
Es claro que para cada cuadro debe haber solo uno de los nueve valores como verdadero (\(1\)) y los otros ocho deben ser falsos (\(0\)).
La segunda regla indica que “cada fila debe contener cada numero del \(1\) al \(9\) una sola vez”, lo cual se puede modelar como
\[\sum_{j=1}^{9} x_{i, j, k} = 1 \text{ para } i = 1\text{, ..., }9\text{, } k = 1\text{, ..., }9\]
Similarmente la tercera regla que dice “Cada columna debe contener cada numero del \(1\) al \(9\) una sola vez se puede modelar asi
\[\sum_{i=1}^{9} x_{i, j, k} = 1 \text{ para } k = 1\text{, ..., }9\text{, } j = 1\text{, ..., }9\]
Para la cuarta regla se tiene que “Cada region de \(3\)x\(3\) debe contener los numeros del \(1\) al \(9\) una sola vez” se puede sumar \(3\) o \(6\) a cada sub indice \(j\) y \(i\), asi se tiene la restricción
\[\sum_{i=1}^{3}\sum_{j=1}^{3} x_{i + U, j + V, k} = 1 \text{ donde } U, V\in\{0, 3, 6\}\]
Finalmente, cada valor inicial (pista) puede ser expresado como una restricción. Suponga que la pista \((i, j)\) es \(m\) para algún \(1\leq m \leq 9\). Entonces \(x_{i, j, m} = 1\). La primera restricción asegura que todos los demás \(x_{i, j, k} = 0\) para \(k \neq m\).
Modelo.
Así pues, el modelo final quedaría como
\[\begin{equation} \begin{aligned} \text{Maximizar } \quad & z = 0 \\ \text{Sujeto a }\quad & \begin{array}{c} \sum_{k=1}^{9} x_{i, j, k} = 1 \text{ para } i = 1\text{, ..., }9\text{, } j = 1\text{, ..., }9 \\ \sum_{j=1}^{9} x_{i, j, k} = 1 \text{ para } i = 1\text{, ..., }9\text{, } k = 1\text{, ..., }9 \\ \sum_{i=1}^{9} x_{i, j, k} = 1 \text{ para } k = 1\text{, ..., }9\text{, } j = 1\text{, ..., }9 \\ \sum_{i=1}^{3}\sum_{j=1}^{3} x_{i + U, j + V, k} = 1 \text{ donde } U, V\in\{0, 3, 6\} \\ x_{i, j, m} = 1 \textbf{ (Todas las pistas) } \\ x_{i, j, k} \in \{0, 1\} \textbf{ (Binarios)} \end{array} \end{aligned} \end{equation}\]
Metodología
En esta sección, abordaremos en detalle la implementación de nuestro
modelo de Programación Lineal Binaria (BILP) para la resolución de
Sudokus utilizando el lenguaje de programación Python. Comenzaremos
explorando las razones detrás de la elección de Python como plataforma
de implementación, en lugar de alternativas como MATLAB. Luego,
presentaremos el paquete PuLP, una biblioteca de
optimización en Python que desempeñará un papel fundamental en la
construcción y resolución de nuestro modelo. Posteriormente,
proporcionaremos un pseudocódigo que esboza los pasos clave que
seguiremos durante la implementación. Finalmente, se mostrara paso a
paso como se realiza la implementación en el lenguaje escogido.
Python y PuLP.
Python se ha convertido en un lenguaje de programación ampliamente reconocido y utilizado en la comunidad científica y de investigación debido a sus ventajas fundamentales. La elección de Python para esta implementación se fundamenta en su simplicidad, facilidad de uso y un amplio conjunto de bibliotecas que se prestan a la resolución de problemas de optimización. Python, como lenguaje de alto nivel, ofrece una sintaxis legible y una curva de aprendizaje suave, lo que lo convierte en una elección lógica para los científicos de datos y los investigadores. Además, Python goza de un apoyo activo por parte de la comunidad, lo que facilita la obtención de ayuda y recursos de programación.
La implementación de nuestro modelo de Programación Lineal Binaria se
beneficiará en gran medida de la inclusión del paquete PuLP
(Python LP). PuLP es una biblioteca de código abierto
escrita en Python que se utiliza para describir problemas de
optimización como modelos matemáticos. PuLP tiene la
capacidad de llamar a uno de los numerosos solucionadores externos de
programación lineal (LP) disponibles, como CBC,
GLPK, CPLEX, Gurobi, entre otros,
para resolver estos modelos. Luego, utiliza comandos en Python para
manipular y mostrar la solución. La historia de PuLP se
remonta a 2007, y a lo largo del tiempo, se ha consolidado como una
herramienta confiable en la comunidad de optimización. PuLP se
caracteriza por su capacidad para abordar problemas de optimización
lineal, entera y binaria, así como su versatilidad para resolver una
amplia variedad de problemas de programación lineal.
Estas son las razones por las cuales se ha decidido utilizar python
con PuLP para implementar el programa
SudokuBILP.
Instalación de PuLP
Nota: Para instalar PuLP se requiere tener una instalación
funcional de Python. Información de cómo instalar Python correctamente
se puede encontrar en esta guía.
PuLP requiere una version de Python mayor o igual a \(2.7\) o \(3.4\).
La version más reciente de PuLP se puede obtener de github.
Instalación usando pip y PyPi
La forma más fácil de instalar PuLP es usando pip.
- En windows (asegurarse de que
pipestá en el PATH)c:\Python34\Scripts\> pip install pulp
- En Linux
$ sudo pip install pulp$ sudo pupltestesto se necesita para que el solver funcione.
Para otros metodos de instalación revisar esta guía.
Una vez instalado la libreria se puede importar desde cualquier linea de comando de python. En algun IDLE o PyDev se puede escribir
import pulp as plp
Ejemplo de LP usando PuLP y Python.
Considerese el siguiente problema de Maximización:
\[\begin{equation} \begin{aligned} \text{Maximizar } \quad & z = 2x + 3y \\ \text{Sujeto a }\quad & \begin{array}{c} x + 2y \leq 8 \\ 3x - y \geq 3 \\ x, y \geq 0 \end{array} \end{aligned} \end{equation}\]
A continuación mostramos como se traduce a Python con
PuLP y como se resuelve.
# Crear un problema LP
problema = plp.LpProblem("Ejemplo1", plp.LpMaximize)
# Definir las variables de decision (x, y)
x = plp.LpVariable('x', lowBound=0)
y = plp.LpVariable('y', lowBound=0)
# Definir la función objetivo
problema += 2 * x + 3 * y, "Función Objetivo"
# Definir las restricciones
problema += x + 2 * y <= 8, "Restriccion_1"
problema += 3 * x - y >= 3, "Restriccion_2"
# Resolver el problema
print(problema.solve())
# Mostrar los resultados
## 1
print("Estado:", plp.LpStatus[problema.status])
## Estado: Optimal
print(f"(x, y, z) = {(x.varValue, y.varValue, plp.value(problema.objective))}")
## (x, y, z) = (8.0, 0.0, 16.0)
De igual forma podemos graficar el problema usando
matplotlib y numpy.
import matplotlib.pyplot as plt
import numpy as np
# Recuperar los valores de las variables de decisión
x_value = x.varValue
y_value = y.varValue
# Definir rangos para graficar
x_range = np.linspace(0, 10, 400) # Crear un rango de valores para x
y_range_1 = (8 - x_range) / 2 # Restricción 1: y = (8 - x) / 2
y_range_2 = 3 * x_range - 3 # Restricción 2: y = 3x - 3
y3 = np.minimum(y_range_1, y_range_2)
# Graficar la solución
plt.plot(x_range, y_range_1, label='Restricción 1: x + 2y <= 8', linestyle='--')
plt.plot(x_range, y_range_2, label='Restricción 2: 3x - y >= 3', linestyle='--')
plt.fill_between(x_range, y3, alpha=0.3, label='Conjunto Factible', color='gray')
plt.scatter(x_value, y_value, color='red', label='Solución Óptima', marker='o')
# Establecer límites y etiquetas del gráfico
plt.xlim(0, 10)
## (0.0, 10.0)
plt.ylim(0, 10)
## (0.0, 10.0)
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(True)
plt.title('Grafico de la Solución Óptima y Restricciones')
# Mostrar el gráfico
plt.show()
Pseudocódigo de Implementación
Para brindar una visión general de cómo estructuraremos nuestra implementación, presentamos un pseudocódigo que detalla los pasos fundamentales que serán seguidos:
- Definir el Problema de Programación Lineal (LP): La implementación se inicia creando un nuevo problema de programación lineal utilizando la biblioteca PuLP.
- Definir la Función Objetivo: Posteriormente, se especificará la función objetivo que buscamos optimizar. En el contexto del Sudoku, como se indicó previamente, la función objetivo no posee un papel crítico, ya que el objetivo principal radica en la búsqueda de una solución factible.
- Definir las Variables de Decisión: Se identificarán y definirán las variables de decisión, que representan las celdas del tablero. Estas variables binarias (0 o 1) denotan si se asigna un número específico a una celda dada.
- Definir las Restricciones: Las restricciones se establecerán para garantizar que la solución cumpla con las reglas del Sudoku, lo que involucra que cada número aparezca solo una vez en cada fila, columna y submatriz de 3x3.
- Resolver el Sudoku: A través de las capacidades de resolución proporcionadas por PuLP, se buscará encontrar una solución que cumpla con todas las restricciones. Si bien no estamos interesados en optimizar una función objetivo específica, la búsqueda se centra en hallar cualquier solución factible.
- Revisar si la Solución es Óptima: A pesar de que nuestra función objetivo no es crítica en este contexto, se procederá a verificar si la solución encontrada es óptima como parte del proceso de validación.
Implementación
Definir el problema LP
Como se vió en el ejemplo anterior, lo primero es crear una istancia
de la clase LpProblem que llamaremos
SudokuBILPr.
# Crear el problema LP
prob = plp.LpProblem("SudokuBILP")
Definir la función objetivo
Después definimos la función objetivo. En nuestro caso, como ya se ha mencionado, solo nos interesa encontrar una solución factible asi que podemos usar la constante \(0\) como nuestra función objetivo.
objective = plp.lpSum(0)
prob.setObjective(objective)
Definir las variables de decisión
Definimos las variables de decisión como
\[\begin{equation*} x_{i, j, k}=\begin{cases} 1 \quad &\text{Si} \, \text{el numero } k \text{ está en la fila } i \text{, columna } j\\ 0 \quad &\text{} \, \text{de otro modo} \\ \end{cases} \end{equation*}\]
rows = range(0,9)
cols = range(0,9)
grids = range(0,9)
values = range(1,10)
# Decision Variable/Target variable
grid_vars = plp.LpVariable.dicts("grid_value", (rows,cols,values), cat='Binary')
Definir las restricciones
Definimos las restricciones que se mencionaron anteriormente.
def add_default_sudoku_constraints(prob, grid_vars, rows, cols, grids, values):
# Constraint to ensure only one value is filled for a cell
for row in rows:
for col in cols:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][col][value] for value in values]),
sense=plp.LpConstraintEQ, rhs=1, name=f"constraint_sum_{row}_{col}"))
# Constraint to ensure that values from 1 to 9 is filled only once in a row
for row in rows:
for value in values:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][col][value]*value for col in cols]),
sense=plp.LpConstraintEQ, rhs=value, name=f"constraint_uniq_row_{row}_{value}"))
# Constraint to ensure that values from 1 to 9 is filled only once in a column
for col in cols:
for value in values:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][col][value]*value for row in rows]),
sense=plp.LpConstraintEQ, rhs=value, name=f"constraint_uniq_col_{col}_{value}"))
# Constraint to ensure that values from 1 to 9 is filled only once in the 3x3 grid
for grid in grids:
grid_row = int(grid/3)
grid_col = int(grid%3)
for value in values:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[grid_row*3+row][grid_col*3+col][value]*value for col in range(0,3) for row in range(0,3)]),
sense=plp.LpConstraintEQ, rhs=value, name=f"constraint_uniq_grid_{grid}_{value}"))
Ademas añadimos las condiciones para la variante diagonal
def add_diagonal_sudoku_constraints(prob, grid_vars, rows, cols, values):
# Constraint from top-left to bottom-right - numbers 1 - 9 should not repeat
for value in values:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][row][value]*value for row in rows]),
sense=plp.LpConstraintEQ, rhs=value, name=f"constraint_uniq_diag1_{value}"))
# Constraint from top-right to bottom-left - numbers 1 - 9 should not repeat
for value in values:
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][len(rows)-row-1][value]*value for row in rows]),
sense=plp.LpConstraintEQ, rhs=value, name=f"constraint_uniq_diag2_{value}"))
Y las pistas
def add_prefilled_constraints(prob, input_sudoku, grid_vars, rows, cols, values):
for row in rows:
for col in cols:
if(input_sudoku[row][col] != 0):
prob.addConstraint(plp.LpConstraint(e=plp.lpSum([grid_vars[row][col][value]*value for value in values]),
sense=plp.LpConstraintEQ,
rhs=input_sudoku[row][col],
name=f"constraint_prefilled_{row}_{col}"))
Extraer e Imprimir la Solución
Creamos funciones para extraer mejor la información
Extraer la solución de las variables de decisión a una lista
def extract_solution(grid_vars, rows, cols, values):
solution = [[0 for col in cols] for row in rows]
grid_list = []
for row in rows:
for col in cols:
for value in values:
if plp.value(grid_vars[row][col][value]):
solution[row][col] = value
return solution
Imprimir las soluciones del Sudoku como una cuadrícula.
def print_solution(solution, rows,cols):
# Print the final result
print(f"\nFinal result:")
print("\n\n+ ----------- + ----------- + ----------- +",end="")
for row in rows:
print("\n",end="\n| ")
for col in cols:
num_end = " | " if ((col+1)%3 == 0) else " "
print(solution[row][col],end=num_end)
if ((row+1)%3 == 0):
print("\n\n+ ----------- + ----------- + ----------- +",end="")
Resolución del Sudoku
Se implementa una función que permita resolver el sudoku usando la implementación ya establecida.
def SudokuBILP(input_sudoku, diagonal = False ):
# Create the linear programming problem
prob = plp.LpProblem("Sudoku_Solver")
rows = range(0,9)
cols = range(0,9)
grids = range(0,9)
values = range(1,10)
# Decision Variable/Target variable
grid_vars = plp.LpVariable.dicts("grid_value", (rows,cols,values), cat='Binary')
# Set the objective function
# Sudoku works only on the constraints - feasibility problem
# There is no objective function that we are trying maximize or minimize.
# Set a dummy objective
objective = plp.lpSum(0)
prob.setObjective(objective)
# Create the default constraints to solve sudoku
add_default_sudoku_constraints(prob, grid_vars, rows, cols, grids, values)
# Add the diagonal constraints if flag is set
if (diagonal):
add_diagonal_sudoku_constraints(prob, grid_vars, rows, cols, values)
# Fill the prefilled values from input sudoku as constraints
add_prefilled_constraints(prob, input_sudoku, grid_vars, rows, cols, values)
# Solve the problem
prob.solve()
# Print the status of the solution
solution_status = plp.LpStatus[prob.status]
print(f'Solution Status = {plp.LpStatus[prob.status]}')
# Extract the solution if an optimal solution has been identified
if solution_status == 'Optimal':
solution = extract_solution(grid_vars, rows, cols, values)
print_solution(solution, rows,cols)
Revisar si se encontro una solución óptima.
Una vez resuelto el problema, se puede revisar si se encontró una
solución óptima por el algoritmo usando su “bandera” de
status. Si el resultado es optimal entonces el
algoritmo LP ha identificado una solución que satisface las condiciones
dadas. Si no se puede encontrar una solución entonces el estatus sería
infeasible (“Infactible”).
print(f'Solution Status = {plp.LpStatus[prob.status]}')
# Code to extract the final solution grid
solution = [[0 for col in cols] for row in rows]
grid_list = []
for row in rows:
for col in cols:
for value in values:
if plp.value(grid_vars[row][col][value]):
solution[row][col] = value
# Print the final solution as a grid
print(f"\nFinal result:")
print("\n\n+ ----------- + ----------- + ----------- +",end="")
for row in rows:
print("\n",end="\n| ")
for col in cols:
num_end = " | " if ((col+1)%3 == 0) else " "
print(solution[row][col],end=num_end)
if ((row+1)%3 == 0):
print("\n\n+ ----------- + ----------- + ----------- +",end="")
Resultados
En esta sección, se expondrán los resultados derivados de la implementación de nuestro modelo de Programación Lineal Binaria (BILP) con el propósito de resolver Sudokus. Se ha desarrollado un algoritmo específico en el entorno Python utilizando el paquete PuLP. El enfoque principal de esta sección consiste en presentar un análisis detallado del rendimiento y la efectividad de nuestro algoritmo al resolver dos Sudokus particulares. Estos ejemplos sirven para ilustrar la aplicación concreta de nuestra metodología en la solución de este enigma numérico. Además, se examinarán críticamente las soluciones resultantes y se llevará a cabo una evaluación de la calidad de los resultados. Estos ejercicios prácticos aportarán una comprensión más profunda del proceso de resolución de Sudokus mediante Programación Lineal y demostrarán cómo nuestra estrategia se traduce en soluciones factibles para este desafiante rompecabezas.
Ejemplo 1
Se resuelve el siguiente sudoku con las reglas normales.
normal_sudoku = [
[3,0,0,8,0,0,0,0,1],
[0,0,0,0,0,2,0,0,0],
[0,4,1,5,0,0,8,3,0],
[0,2,0,0,0,1,0,0,0],
[8,5,0,4,0,3,0,1,7],
[0,0,0,7,0,0,0,2,0],
[0,8,5,0,0,9,7,4,0],
[0,0,0,1,0,0,0,0,0],
[9,0,0,0,0,7,0,0,6]
]
SudokuBILP(input_sudoku=normal_sudoku, diagonal=False)
## Solution Status = Optimal
##
## Final result:
##
##
## + ----------- + ----------- + ----------- +
##
## | 3 6 7 | 8 9 4 | 2 5 1 |
##
## | 5 9 8 | 3 1 2 | 6 7 4 |
##
## | 2 4 1 | 5 7 6 | 8 3 9 |
##
## + ----------- + ----------- + ----------- +
##
## | 7 2 3 | 9 8 1 | 4 6 5 |
##
## | 8 5 6 | 4 2 3 | 9 1 7 |
##
## | 4 1 9 | 7 6 5 | 3 2 8 |
##
## + ----------- + ----------- + ----------- +
##
## | 1 8 5 | 6 3 9 | 7 4 2 |
##
## | 6 7 2 | 1 4 8 | 5 9 3 |
##
## | 9 3 4 | 2 5 7 | 1 8 6 |
##
## + ----------- + ----------- + ----------- +
Ejemplo 2
Se resuelve el siguiente sudoku usando la regla de las diagonales.
diagonal_sudoku = [
[0,3,0,2,7,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[8,0,0,0,0,0,0,0,0],
[5,1,0,0,0,0,0,8,4],
[4,0,0,5,9,0,0,7,0],
[2,9,0,0,0,0,0,1,0],
[0,0,0,0,0,0,1,0,5],
[0,0,6,3,0,8,0,0,7],
[0,0,0,0,0,0,3,0,0]
]
SudokuBILP(input_sudoku=diagonal_sudoku, diagonal=True)
## Solution Status = Optimal
##
## Final result:
##
##
## + ----------- + ----------- + ----------- +
##
## | 6 3 9 | 2 7 4 | 8 5 1 |
##
## | 1 4 2 | 6 8 5 | 7 3 9 |
##
## | 8 7 5 | 1 3 9 | 6 4 2 |
##
## + ----------- + ----------- + ----------- +
##
## | 5 1 3 | 7 6 2 | 9 8 4 |
##
## | 4 6 8 | 5 9 1 | 2 7 3 |
##
## | 2 9 7 | 8 4 3 | 5 1 6 |
##
## + ----------- + ----------- + ----------- +
##
## | 3 8 4 | 9 2 7 | 1 6 5 |
##
## | 9 5 6 | 3 1 8 | 4 2 7 |
##
## | 7 2 1 | 4 5 6 | 3 9 8 |
##
## + ----------- + ----------- + ----------- +
Conclusión
La implementación de un modelo de Programación Lineal Binaria (BILP) para la resolución de Sudokus a través de Python y el paquete PuLP ha demostrado ser un enfoque efectivo, sencillo y práctico. Los resultados obtenidos en la resolución de los ejemplos de Sudokus presentados en este estudio respaldan la viabilidad de esta metodología. Se ha demostrado que es posible modelar con precisión las reglas y restricciones del Sudoku como un conjunto de ecuaciones y desigualdades lineales, permitiendo así la representación del problema como un BILP.
Este enfoque ofrece varias ventajas notables, como la simplicidad en la implementación, la flexibilidad en la elección de los Sudokus a resolver y la rapidez en la obtención de soluciones factibles. La función objetivo en este contexto no tiene una relevancia crítica, ya que el principal objetivo es encontrar una solución factible que cumpla con las restricciones impuestas por el Sudoku.
En resumen, este estudio demuestra que abordar la resolución de Sudokus como un problema de Programación Lineal Binaria, junto con la implementación en Python mediante el uso de PuLP, es una estrategia viable y efectiva. Este enfoque no solo se presenta como un interesante ejercicio de programación, sino que también puede ser útil en aplicaciones prácticas para resolver Sudokus y desafíos similares.